From 4eafe6e40a7cf9fe54f99061b88eae56c5843673 Mon Sep 17 00:00:00 2001 From: Manuel Gugger Date: Fri, 13 Jan 2023 21:11:42 +0100 Subject: [PATCH] implement column sorting --- actix_admin_macros/src/lib.rs | 22 +++- actix_admin_macros/src/struct_fields.rs | 26 ++++- src/builder.rs | 2 +- src/lib.rs | 4 +- src/model.rs | 5 +- src/routes/delete.rs | 27 ++++- src/routes/list.rs | 19 ++-- src/routes/mod.rs | 2 +- src/view_model.rs | 6 +- static/js/default.js | 12 +-- templates/base.html | 2 +- templates/list.html | 127 +++++++++++++----------- templates/navbar.html | 4 +- tests/get_request_is_success.rs | 9 +- 14 files changed, 172 insertions(+), 95 deletions(-) diff --git a/actix_admin_macros/src/lib.rs b/actix_admin_macros/src/lib.rs index 9058116..fb8244f 100644 --- a/actix_admin_macros/src/lib.rs +++ b/actix_admin_macros/src/lib.rs @@ -68,8 +68,8 @@ pub fn derive_actix_admin_view_model(input: proc_macro::TokenStream) -> proc_mac #[actix_admin::prelude::async_trait(?Send)] impl ActixAdminViewModelTrait for Entity { - async fn list(db: &DatabaseConnection, page: u64, entities_per_page: u64, search: &String) -> Result<(u64, Vec), ActixAdminError> { - let entities = Entity::list_model(db, page, entities_per_page, search).await; + async fn list(db: &DatabaseConnection, page: u64, entities_per_page: u64, search: &str, sort_by: &str, sort_order: &SortOrder) -> Result<(u64, Vec), ActixAdminError> { + let entities = Entity::list_model(db, page, entities_per_page, search, sort_by, sort_order).await; entities } @@ -155,6 +155,7 @@ pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::T let fields_type_path = get_actix_admin_fields_type_path_string(&fields); let fields_textarea = get_actix_admin_fields_textarea(&fields); let fields_file_upload = get_actix_admin_fields_file_upload(&fields); + let fields_match_name_to_columns = get_match_name_to_column(&fields); let expanded = quote! { actix_admin::prelude::lazy_static! { @@ -236,14 +237,25 @@ pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::T #[actix_admin::prelude::async_trait] impl ActixAdminModelTrait for Entity { - async fn list_model(db: &DatabaseConnection, page: u64, posts_per_page: u64, search: &String) -> Result<(u64, Vec), ActixAdminError> { + async fn list_model(db: &DatabaseConnection, page: u64, posts_per_page: u64, search: &str, sort_by: &str, sort_order: &SortOrder) -> Result<(u64, Vec), ActixAdminError> { use sea_orm::{ query::* }; - let paginator = Entity::find() + + let sort_column = match sort_by { + #(#fields_match_name_to_columns)* + _ => panic!("Unknown column") + }; + + let query = if sort_order.eq(&SortOrder::Asc) { + Entity::find().order_by_asc(sort_column) + } else { + Entity::find().order_by_desc(sort_column) + }; + + let paginator = query .filter( Condition::any() #(#fields_searchable)* ) - .order_by_asc(Column::Id) .paginate(db, posts_per_page); let num_pages = paginator.num_pages().await?; let mut model_entities = Vec::new(); diff --git a/actix_admin_macros/src/struct_fields.rs b/actix_admin_macros/src/struct_fields.rs index 91e1834..2e193ed 100644 --- a/actix_admin_macros/src/struct_fields.rs +++ b/actix_admin_macros/src/struct_fields.rs @@ -17,7 +17,15 @@ pub fn get_fields_for_tokenstream(input: proc_macro::TokenStream) -> std::vec::V } fn capitalize_first_letter(s: &str) -> String { - s[0..1].to_uppercase() + &s[1..] + if s.len() > 0 { + s[0..1].to_uppercase() + &s[1..] + } else { + String::new() + } +} + +fn to_camelcase(s: &str) -> String { + s.split("_").fold(String::new(), |a, b| capitalize_first_letter(&a) + &capitalize_first_letter(b)) } pub fn filter_fields(fields: &Fields) -> Vec { @@ -208,12 +216,26 @@ pub fn get_actix_admin_fields_file_upload(fields: &Vec) -> Vec>() } +pub fn get_match_name_to_column(fields: &Vec) -> Vec { + fields + .iter() + .map(|model_field| { + let column_name = model_field.ident.to_string(); + let column_name_capitalized = to_camelcase(&column_name); + let column_ident = Ident::new(&column_name_capitalized, Span::call_site()); + quote! { + #column_name => Column::#column_ident, + } + }) + .collect::>() +} + pub fn get_actix_admin_fields_searchable(fields: &Vec) -> Vec { 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_name = 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)) diff --git a/src/builder.rs b/src/builder.rs index 0f943d7..f81e353 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -146,7 +146,7 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder { if add_to_menu { let menu_element = ActixAdminMenuElement { name: menu_element_name.to_string(), - link: format!("{}", path.replacen("/", "", 1)), + link: path.replacen("/", "", 1), is_custom_handler: true, }; let category = self.actix_admin.entity_names.get_mut(""); diff --git a/src/lib.rs b/src/lib.rs index cc58f70..621f670 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,7 +6,7 @@ use lazy_static::lazy_static; use sea_orm::DatabaseConnection; -use serde::Serialize; +use serde::{Serialize }; use std::collections::HashMap; use tera::{Tera, Result, to_value, try_get_value }; use std::{ hash::BuildHasher}; @@ -31,7 +31,7 @@ pub mod prelude { pub use actix_admin_macros::{ DeriveActixAdmin, DeriveActixAdminModel, DeriveActixAdminViewModel, DeriveActixAdminEnumSelectList, DeriveActixAdminModelSelectList }; pub use crate::{ ActixAdminError, ActixAdminAppDataTrait, ActixAdmin, ActixAdminConfiguration }; pub use crate::{ hashmap, ActixAdminSelectListTrait }; - pub use crate::routes::{ create_or_edit_post, get_admin_ctx }; + pub use crate::routes::{ create_or_edit_post, get_admin_ctx, SortOrder }; pub use crate::{ TERA }; pub use itertools::izip; pub use lazy_static::lazy_static; diff --git a/src/model.rs b/src/model.rs index cc846eb..3329207 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,3 +1,4 @@ +use crate::routes::SortOrder; use crate::{ActixAdminError, ActixAdminViewModelField}; use actix_multipart::{Multipart, MultipartError}; use actix_web::web::Bytes; @@ -17,7 +18,9 @@ pub trait ActixAdminModelTrait { db: &DatabaseConnection, page: u64, posts_per_page: u64, - search: &String, + search: &str, + sort_by: &str, + sort_order: &SortOrder ) -> Result<(u64, Vec), ActixAdminError>; fn get_fields() -> &'static [ActixAdminViewModelField]; fn validate_model(model: &mut ActixAdminModel); diff --git a/src/routes/delete.rs b/src/routes/delete.rs index 6c05402..ad658a2 100644 --- a/src/routes/delete.rs +++ b/src/routes/delete.rs @@ -57,7 +57,7 @@ pub async fn delete_many session: Session, _req: HttpRequest, data: web::Data, - form: web::Form>, + form: web::Form>, ) -> Result { let actix_admin = data.get_actix_admin(); let entity_name = E::get_entity_name(); @@ -74,7 +74,7 @@ pub async fn delete_many let db = &data.get_db(); let entity_name = E::get_entity_name(); - let ids: Vec = form.iter().map(|el| el.1).collect(); + let ids: Vec = form.iter().filter(|el| el.0 == "ids").map(|el| el.1.parse::().unwrap()).collect(); // TODO: implement delete_many for id in ids { @@ -104,11 +104,32 @@ pub async fn delete_many } } + let entities_per_page = form.iter() + .find(|el| el.0 == "entities_per_page") + .map(|e| e.1.to_string()) + .unwrap_or("10".to_string()); + let search = form.iter() + .find(|el| el.0 == "search") + .map(|e| e.1.to_string()) + .unwrap_or_default(); + let sort_by = form.iter() + .find(|el| el.0 == "sort_by") + .map(|e| e.1.to_string()) + .unwrap_or("id".to_string()); + let sort_order = form.iter() + .find(|el| el.0 == "sort_order") + .map(|e| e.1.to_string()) + .unwrap_or("Asc".to_string()); + let page = form.iter() + .find(|el| el.0 == "page") + .map(|e| e.1.to_string()) + .unwrap_or("1".to_string()); + match errors.is_empty() { true => Ok(HttpResponse::SeeOther() .append_header(( header::LOCATION, - format!("/admin/{}/list?render_partial=true", entity_name), + format!("/admin/{}/list?render_partial=true&entities_per_page={}&search={}&sort_by={}&sort_order={}&page={}", entity_name, entities_per_page, search, sort_by, sort_order, page), )) .finish()), false => Ok(HttpResponse::InternalServerError().finish()), diff --git a/src/routes/list.rs b/src/routes/list.rs index f838ac4..ac85423 100644 --- a/src/routes/list.rs +++ b/src/routes/list.rs @@ -2,7 +2,7 @@ use actix_web::{error, web, Error, HttpRequest, HttpResponse}; use serde::Serialize; use serde::{Deserialize}; use tera::{Context}; -use crate::prelude::*; +use crate::{prelude::*}; use crate::ActixAdminViewModelTrait; use crate::ActixAdminViewModel; @@ -14,12 +14,6 @@ use super::{ add_auth_context, user_can_access_page, render_unauthorized}; const DEFAULT_ENTITIES_PER_PAGE: u64 = 10; -#[derive(Debug, Serialize, Deserialize)] -pub enum SortOrder { - Asc, - Desc, -} - #[derive(Debug, Deserialize)] pub struct Params { page: Option, @@ -30,6 +24,12 @@ pub struct Params { sort_order: Option } +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum SortOrder { + Asc, + Desc, +} + pub async fn list( session: Session, req: HttpRequest, @@ -61,7 +61,8 @@ pub async fn list( let db = data.get_db(); let sort_by = params.sort_by.clone().unwrap_or(view_model.primary_key.to_string()); let sort_order = params.sort_order.as_ref().unwrap_or(&SortOrder::Asc); - let result = E::list(db, page, entities_per_page, &search).await; + + let result = E::list(db, page, entities_per_page, &search, &sort_by, &sort_order).await; match result { Ok(res) => { let entities = res.1; @@ -101,8 +102,6 @@ pub async fn list( ctx.insert("search", &search); ctx.insert("sort_by", &sort_by); ctx.insert("sort_order", &sort_order); - ctx.insert("sort_order_asc", &SortOrder::Asc); - ctx.insert("sort_order_desc", &SortOrder::Desc); let body = TERA .render("list.html", &ctx) diff --git a/src/routes/mod.rs b/src/routes/mod.rs index adcd615..52a96d7 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -8,7 +8,7 @@ mod index; pub use index::{ index, not_found, get_admin_ctx }; mod list; -pub use list::{ list }; +pub use list::{ list, SortOrder }; mod show; pub use show::show; diff --git a/src/view_model.rs b/src/view_model.rs index 1eb976a..ff9e339 100644 --- a/src/view_model.rs +++ b/src/view_model.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use sea_orm::DatabaseConnection; use serde::{Serialize, Deserialize}; use std::collections::HashMap; -use crate::ActixAdminModel; +use crate::{ActixAdminModel, SortOrder}; use actix_session::{Session}; use std::convert::From; use crate::ActixAdminError; @@ -13,7 +13,9 @@ pub trait ActixAdminViewModelTrait { db: &DatabaseConnection, page: u64, entities_per_page: u64, - search: &String + search: &str, + sort_by: &str, + sort_order: &SortOrder ) -> Result<(u64, Vec), ActixAdminError>; // TODO: Replace return value with proper Result Type containing Ok or Err diff --git a/static/js/default.js b/static/js/default.js index 3019ba1..2b5f172 100644 --- a/static/js/default.js +++ b/static/js/default.js @@ -31,14 +31,14 @@ function checkAll(bx) { } function sort_by(column) { - document.getElementById("sort_by").value = column; - current_sort_order = document.getElementById("sort_order").value; - if (current_sort_order == "{{ sort_order_asc }}") { - document.getElementById("sort_order").value = "{{ sort_order_desc }}"; + current_sort_order = document.getElementsByName("sort_order")[0].value; + if (current_sort_order == "Asc") { + document.getElementsByName("sort_order").forEach((e) => e.value = "Desc"); } else { - document.getElementById("sort_order").value = "{{ sort_order_asc }}"; + document.getElementsByName("sort_order").forEach((e) => e.value = "Asc"); } - document.getElementById('search_form').submit(); + document.getElementsByName("sort_by").forEach((e) => e.value = column); + document.getElementById('table_form').requestSubmit(); } document.addEventListener('DOMContentLoaded', () => { diff --git a/templates/base.html b/templates/base.html index 7295f52..3550682 100644 --- a/templates/base.html +++ b/templates/base.html @@ -24,7 +24,7 @@ {% endfor %} {% endif %} -
+
{% block content %} {% endblock content %}
diff --git a/templates/list.html b/templates/list.html index af412c9..1c61c50 100644 --- a/templates/list.html +++ b/templates/list.html @@ -8,7 +8,7 @@
Create - -
- {% if view_model.show_search %} -
-

- - - - -

-
- {% endif %} -
-
-
-
- - - + + + +
+
+ {% if view_model.show_search %} +

+ + + + +

+ {% endif %}
-
- {% for a in [10,20,50,100,] %} {% endfor %} @@ -59,19 +56,24 @@

Entities per Page

- +
-
+
- {% endif %}
-
-
- {% include "loader.html" %} - - +
+ {% include "loader.html" %} + + + + + + +
+ - - + + + {% for entity in entities -%} @@ -125,37 +128,47 @@ {% endif %} {%- endfor %} {%- endfor %} - - - - - - -
@@ -79,9 +81,9 @@ {{ view_model.primary_key | title }} {% if sort_by == view_model.primary_key %} - {% if sort_order == "{{ sort_order_asc }}" %} + {% if sort_order == "Asc" %} - {% elif sort_order == "{{ sort_order_desc }}" %} + {% elif sort_order == "Desc" %} {% endif %} {% endif %} @@ -90,9 +92,9 @@ {{ model_field.field_name | split(pat="_") | join(sep=" ") | title }} {% if sort_by == model_field.field_name %} - {% if sort_order == "{{ sort_order_asc }}" %} + {% if sort_order == "Asc" %} - {% elif sort_order == "{{ sort_order_desc }}" %} + {% elif sort_order == "Desc" %} {% endif %} {% endif %} @@ -103,8 +105,9 @@
- - + +
-
-
+ + + + + + + + -
-