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 primary_key: Option<()>,
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>,
// Anything that implements `syn::parse::Parse` is supported.

View File

@ -13,7 +13,8 @@ use struct_fields::{
get_primary_key_field_name,
get_actix_admin_fields_select_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;
@ -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 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 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)]
impl ActixAdminViewModelTrait for Entity {
async fn list(db: &DatabaseConnection, page: usize, entities_per_page: usize) -> (usize, Vec<ActixAdminModel>) {
let entities = Entity::list_model(db, page, entities_per_page).await;
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, search).await;
entities
}
@ -163,9 +165,13 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
#[async_trait]
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::* };
let paginator = Entity::find()
.filter(
Condition::any()
#(#fields_searchable)*
)
.order_by_asc(Column::Id)
.paginate(db, posts_per_page);
let num_pages = paginator.num_pages().await.ok().unwrap();

View File

@ -11,7 +11,8 @@ pub struct ModelField {
pub inner_type: Option<Type>,
pub primary_key: bool,
pub html_input_type: String,
pub select_list: String
pub select_list: String,
pub searchable: bool
}
impl ModelField {

View File

@ -1,8 +1,8 @@
use crate::attributes::derive_attr;
use crate::model_fields::ModelField;
use proc_macro2::TokenStream;
use proc_macro2::{Span, TokenStream};
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> {
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
}
fn capitalize_first_letter(s: &str) -> String {
s[0..1].to_uppercase() + &s[1..]
}
pub fn filter_fields(fields: &Fields) -> Vec<ModelField> {
fields
.iter()
@ -31,6 +35,9 @@ pub fn filter_fields(fields: &Fields) -> Vec<ModelField> {
let is_primary_key = actix_admin_attr
.clone()
.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| {
attr.select_list.map_or("".to_string(), |attr_field| {
(LitStr::from(attr_field)).value()
@ -51,6 +58,7 @@ pub fn filter_fields(fields: &Fields) -> Vec<ModelField> {
primary_key: is_primary_key,
html_input_type: html_input_type,
select_list: select_list,
searchable: is_searchable
};
Some(model_field)
} else {
@ -146,6 +154,20 @@ pub fn get_actix_admin_fields_html_input(fields: &Vec<ModelField>) -> Vec<TokenS
.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> {
fields
.iter()

View File

@ -10,6 +10,7 @@ pub trait ActixAdminModelTrait {
db: &DatabaseConnection,
page: usize,
posts_per_page: usize,
search: &String
) -> (usize, Vec<ActixAdminModel>);
fn get_fields() -> Vec<ActixAdminViewModelField>;
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
.unwrap_or(DEFAULT_ENTITIES_PER_PAGE);
let render_partial = params.render_partial.unwrap_or(false);
let search = params.search.clone().unwrap_or(String::new());
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 num_pages = result.0;

View File

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

View File

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

View File

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

View File

@ -5,24 +5,22 @@
{% if not render_partial or render_partial == false %}
<article>
<div class="grid">
<div>
<a href="create" role="button">Create</a>
</div>
<!--
<div>
<details role="list">
<summary aria-haspopup="listbox" role="button">
With selected
Actions
</summary>
<ul role="listbox">
<li><a>Delete</a></li>
<li><a href="create">Create</a></li>
<li><a>Delete Selected</a></li>
</ul>
</details>
</div>-->
</div>
<div>
<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-trigger="keyup changed delay:500ms, search" hx-target="#{{ entity_name }}table"
hx-indicator=".htmx-indicator"
>
</div>
</div>
@ -32,7 +30,7 @@
<thead>
<tr>
<th>
<!-- Select Checkbox -->
<div class="htmx-indicator">{% include "spinner.svg" %}</div>
</th>
<th>{{ view_model.primary_key | title }}</th>
{% 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)]
#[actix_admin(primary_key)]
pub id: i32,
#[actix_admin(searchable)]
pub title: String,
#[sea_orm(column_type = "Text")]
#[actix_admin(searchable)]
pub text: String,
#[actix_admin(select_list="Tea")]
pub tea_mandatory: Tea,