add custom handlers and add them optionally to the menu

This commit is contained in:
Manuel Gugger 2022-10-11 18:54:45 +02:00
parent 859c139f8b
commit b39ad99c7f
7 changed files with 166 additions and 73 deletions

View File

@ -4,16 +4,16 @@
## Getting Started ## Getting Started
* See the [example](https://github.com/mgugger/actix-admin/tree/main/example). * See the [example](https://github.com/mgugger/actix-admin/tree/main/example) and run with ```cargo run```.
* See the step by [step tutorial](https://github.com/mgugger/actix-admin/tree/main/example/StepbyStep.md) * See the step by [step tutorial](https://github.com/mgugger/actix-admin/tree/main/example/StepbyStep.md)
## Features ## Features
1. Async, builds on [sea-orm](https://crates.io/crates/sea-orm) for the database backend 1. Async: Builds on [sea-orm](https://crates.io/crates/sea-orm) for the database backend
2. Macros, generate the required implementations for models automatically 2. Macros: Generate the required implementations for models automatically
3. Authentication, optionally pass authentication handler to implement authentication for views 3. Authentication: optionally pass authentication handler to implement authentication for views
4. Supports custom validation rules 4. Supports custom validation rules
5. Searchable attributes can be specified 5. Searchable attributes can be specified
6. Supports a custom index view 6. Supports custom views which are added to the Navbar
## Screenshot ## Screenshot

View File

@ -113,10 +113,12 @@ fn create_actix_admin_builder() -> ActixAdminBuilder {
}; };
let mut admin_builder = ActixAdminBuilder::new(configuration); let mut admin_builder = ActixAdminBuilder::new(configuration);
admin_builder.add_custom_handler_for_index::<AppState>( // admin_builder.add_custom_handler_for_index::<AppState>(
web::get().to(custom_index::<AppState>) // web::get().to(custom_index::<AppState>)
); // );
admin_builder.add_entity::<AppState, Post>(&post_view_model); admin_builder.add_entity::<AppState, Post>(&post_view_model);
admin_builder.add_custom_handler("Custom Route in Menu", "/custom_route_in_menu", web::get().to(custom_index::<AppState>), true);
admin_builder.add_custom_handler("Custom Route not in Menu", "/custom_route_not_in_menu", web::get().to(custom_index::<AppState>), false);
let some_category = "Some Category"; let some_category = "Some Category";
admin_builder.add_entity_to_category::<AppState, Comment>(&comment_view_model, some_category); admin_builder.add_entity_to_category::<AppState, Comment>(&comment_view_model, some_category);
@ -124,7 +126,8 @@ fn create_actix_admin_builder() -> ActixAdminBuilder {
"My custom handler", "My custom handler",
"/custom_handler", "/custom_handler",
web::get().to(custom_handler::<AppState, Comment>), web::get().to(custom_handler::<AppState, Comment>),
some_category some_category,
true
); );
admin_builder admin_builder

View File

@ -1,14 +1,17 @@
use actix_web::{ web, Route };
use std::collections::HashMap;
use crate::{prelude::*, ActixAdminMenuElement}; use crate::{prelude::*, ActixAdminMenuElement};
use actix_web::{web, Route};
use std::collections::HashMap;
use crate::routes::{create_get, create_post, not_found, delete, delete_many, edit_get, edit_post, index, list, show}; use crate::routes::{
create_get, create_post, delete, delete_many, edit_get, edit_post, index, list, not_found, show,
};
/// Represents a builder entity which helps generating the ActixAdmin configuration /// Represents a builder entity which helps generating the ActixAdmin configuration
pub struct ActixAdminBuilder { pub struct ActixAdminBuilder {
pub scopes: HashMap<String, actix_web::Scope>, pub scopes: HashMap<String, actix_web::Scope>,
pub custom_routes: Vec<(String, Route)>,
pub actix_admin: ActixAdmin, pub actix_admin: ActixAdmin,
pub custom_index: Option<Route> pub custom_index: Option<Route>,
} }
/// The trait to work with ActixAdminBuilder /// The trait to work with ActixAdminBuilder
@ -18,28 +21,43 @@ pub trait ActixAdminBuilderTrait {
&mut self, &mut self,
view_model: &ActixAdminViewModel, view_model: &ActixAdminViewModel,
); );
fn add_entity_to_category<T: ActixAdminAppDataTrait + 'static, E: ActixAdminViewModelTrait + 'static>( fn add_entity_to_category<
T: ActixAdminAppDataTrait + 'static,
E: ActixAdminViewModelTrait + 'static,
>(
&mut self, &mut self,
view_model: &ActixAdminViewModel, view_model: &ActixAdminViewModel,
category_name: &str category_name: &str,
); );
fn add_custom_handler_for_entity<T: ActixAdminAppDataTrait + 'static, E: ActixAdminViewModelTrait + 'static>( fn add_custom_handler(
&mut self,
menu_element_name: &str,
path: &str,
route: Route
);
fn add_custom_handler_for_entity_in_category<T: ActixAdminAppDataTrait + 'static, E: ActixAdminViewModelTrait + 'static>(
&mut self, &mut self,
menu_element_name: &str, menu_element_name: &str,
path: &str, path: &str,
route: Route, route: Route,
category_name: &str add_to_menu: bool,
); );
fn add_custom_handler_for_index<T: ActixAdminAppDataTrait + 'static>( fn add_custom_handler_for_entity<
T: ActixAdminAppDataTrait + 'static,
E: ActixAdminViewModelTrait + 'static,
>(
&mut self, &mut self,
route: Route menu_element_name: &str,
path: &str,
route: Route,
add_to_menu: bool
); );
fn add_custom_handler_for_entity_in_category<
T: ActixAdminAppDataTrait + 'static,
E: ActixAdminViewModelTrait + 'static,
>(
&mut self,
menu_element_name: &str,
path: &str,
route: Route,
category_name: &str,
add_to_menu: bool,
);
fn add_custom_handler_for_index<T: ActixAdminAppDataTrait + 'static>(&mut self, route: Route);
fn get_scope<T: ActixAdminAppDataTrait + 'static>(self) -> actix_web::Scope; fn get_scope<T: ActixAdminAppDataTrait + 'static>(self) -> actix_web::Scope;
fn get_actix_admin(&self) -> ActixAdmin; fn get_actix_admin(&self) -> ActixAdmin;
} }
@ -50,10 +68,11 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder {
actix_admin: ActixAdmin { actix_admin: ActixAdmin {
entity_names: HashMap::new(), entity_names: HashMap::new(),
view_models: HashMap::new(), view_models: HashMap::new(),
configuration: configuration configuration: configuration,
}, },
custom_routes: Vec::new(),
scopes: HashMap::new(), scopes: HashMap::new(),
custom_index: None custom_index: None,
} }
} }
@ -64,10 +83,13 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder {
let _ = &self.add_entity_to_category::<T, E>(view_model, ""); let _ = &self.add_entity_to_category::<T, E>(view_model, "");
} }
fn add_entity_to_category<T: ActixAdminAppDataTrait + 'static, E: ActixAdminViewModelTrait + 'static>( fn add_entity_to_category<
T: ActixAdminAppDataTrait + 'static,
E: ActixAdminViewModelTrait + 'static,
>(
&mut self, &mut self,
view_model: &ActixAdminViewModel, view_model: &ActixAdminViewModel,
category_name: &str category_name: &str,
) { ) {
self.scopes.insert( self.scopes.insert(
E::get_entity_name(), E::get_entity_name(),
@ -80,23 +102,23 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder {
.route("/delete", web::delete().to(delete_many::<T, E>)) .route("/delete", web::delete().to(delete_many::<T, E>))
.route("/delete/{id}", web::delete().to(delete::<T, E>)) .route("/delete/{id}", web::delete().to(delete::<T, E>))
.route("/show/{id}", web::get().to(show::<T, E>)) .route("/show/{id}", web::get().to(show::<T, E>))
.default_service(web::to(not_found)) .default_service(web::to(not_found)),
); );
let category = self.actix_admin.entity_names.get_mut(category_name); let category = self.actix_admin.entity_names.get_mut(category_name);
let menu_element = ActixAdminMenuElement { let menu_element = ActixAdminMenuElement {
name: E::get_entity_name(), name: E::get_entity_name(),
link: E::get_entity_name(), link: E::get_entity_name(),
is_custom_handler: false is_custom_handler: false,
}; };
match category { match category {
Some(entity_list) => { Some(entity_list) => entity_list.push(menu_element),
entity_list.push(menu_element)
},
None => { None => {
let mut entity_list = Vec::new(); let mut entity_list = Vec::new();
entity_list.push(menu_element); entity_list.push(menu_element);
self.actix_admin.entity_names.insert(category_name.to_string(), entity_list); self.actix_admin
.entity_names
.insert(category_name.to_string(), entity_list);
} }
} }
@ -104,33 +126,71 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder {
self.actix_admin.view_models.insert(key, view_model.clone()); self.actix_admin.view_models.insert(key, view_model.clone());
} }
fn add_custom_handler_for_index<T: ActixAdminAppDataTrait + 'static>( fn add_custom_handler_for_index<T: ActixAdminAppDataTrait + 'static>(&mut self, route: Route) {
&mut self,
route: Route
) {
self.custom_index = Some(route); self.custom_index = Some(route);
} }
fn add_custom_handler_for_entity<T: ActixAdminAppDataTrait + 'static, E: ActixAdminViewModelTrait + 'static>( fn add_custom_handler(
&mut self, &mut self,
menu_element_name: &str, menu_element_name: &str,
path: &str, path: &str,
route: Route route: Route,
) { add_to_menu: bool,
let _ = &self.add_custom_handler_for_entity_in_category::<T,E>(menu_element_name ,path, route, ""); ) {
self.custom_routes.push((path.to_string(), route));
if add_to_menu {
let menu_element = ActixAdminMenuElement {
name: menu_element_name.to_string(),
link: format!("{}", path.replacen("/", "", 1)),
is_custom_handler: true,
};
let category = self.actix_admin.entity_names.get_mut("");
match category {
Some(entity_list) => {
if !entity_list.contains(&menu_element) {
entity_list.push(menu_element);
}
}
_ => (),
}
}
} }
fn add_custom_handler_for_entity_in_category<T: ActixAdminAppDataTrait + 'static, E: ActixAdminViewModelTrait + 'static>( fn add_custom_handler_for_entity<
T: ActixAdminAppDataTrait + 'static,
E: ActixAdminViewModelTrait + 'static,
>(
&mut self, &mut self,
menu_element_name: &str, menu_element_name: &str,
path: &str, path: &str,
route: Route, route: Route,
category_name: &str add_to_menu: bool,
) {
let _ = &self.add_custom_handler_for_entity_in_category::<T, E>(
menu_element_name,
path,
route,
"",
add_to_menu,
);
}
fn add_custom_handler_for_entity_in_category<
T: ActixAdminAppDataTrait + 'static,
E: ActixAdminViewModelTrait + 'static,
>(
&mut self,
menu_element_name: &str,
path: &str,
route: Route,
category_name: &str,
add_to_menu: bool,
) { ) {
let menu_element = ActixAdminMenuElement { let menu_element = ActixAdminMenuElement {
name: menu_element_name.to_string(), name: menu_element_name.to_string(),
link: format!("{}{}", E::get_entity_name(), path), link: format!("{}{}", E::get_entity_name(), path),
is_custom_handler: true is_custom_handler: true,
}; };
let existing_scope = self.scopes.remove(&E::get_entity_name()); let existing_scope = self.scopes.remove(&E::get_entity_name());
@ -138,38 +198,44 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder {
match existing_scope { match existing_scope {
Some(scope) => { Some(scope) => {
let existing_scope = scope.route(path, route); let existing_scope = scope.route(path, route);
self.scopes.insert(menu_element.link.to_string(), existing_scope); self.scopes
}, .insert(menu_element.link.to_string(), existing_scope);
}
_ => { _ => {
let new_scope = let new_scope =
web::scope(&format!("/{}", E::get_entity_name())) web::scope(&format!("/{}", E::get_entity_name())).route(path, route);
.route(path, route);
self.scopes.insert(menu_element.link.to_string(), new_scope); self.scopes.insert(menu_element.link.to_string(), new_scope);
} }
} }
let category = self.actix_admin.entity_names.get_mut(category_name); if add_to_menu {
match category { let category = self.actix_admin.entity_names.get_mut(category_name);
Some(entity_list) => { match category {
if !entity_list.contains(&menu_element) { Some(entity_list) => {
entity_list.push(menu_element); if !entity_list.contains(&menu_element) {
entity_list.push(menu_element);
}
} }
_ => (),
} }
_ => (),
} }
} }
fn get_scope<T: ActixAdminAppDataTrait + 'static>(self) -> actix_web::Scope { fn get_scope<T: ActixAdminAppDataTrait + 'static>(self) -> actix_web::Scope {
let index_handler = match self.custom_index { let index_handler = match self.custom_index {
Some(handler) => handler, Some(handler) => handler,
_ => web::get().to(index::<T>) _ => web::get().to(index::<T>),
}; };
let mut admin_scope = web::scope("/admin") let mut admin_scope = web::scope("/admin")
.route("/", index_handler) .route("/", index_handler)
.default_service(web::to(not_found)); .default_service(web::to(not_found));
for (_entity, scope) in self.scopes { for (_entity, scope) in self.scopes {
admin_scope = admin_scope.service(scope); admin_scope = admin_scope.service(scope);
}
for (path, route) in self.custom_routes {
admin_scope = admin_scope.route(&path, route);
} }
admin_scope admin_scope

View File

@ -20,15 +20,17 @@
<div id="notifications"> <div id="notifications">
{% if notifications %} {% if notifications %}
{% for notification in notifications -%} {% for notification in notifications -%}
<div class="notification mb-4 is-light {{ notification.css_class }}"> <div class="notification mb-4 is-light {{ notification.css_class }}">
<button class="delete" onclick="this.parentElement.remove()"></button> <button class="delete" onclick="this.parentElement.remove()"></button>
{{ notification.message }} {{ notification.message }}
</div> </div>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</div> </div>
{% block content %} <div class="fade-in">
{% endblock content %} {% block content %}
{% endblock content %}
</div>
</div> </div>
</body> </body>

View File

@ -41,7 +41,7 @@
htmx.on("htmx:responseError", function () { htmx.on("htmx:responseError", function () {
document.getElementById("notifications").insertAdjacentHTML( document.getElementById("notifications").insertAdjacentHTML(
"afterend", "afterend",
"<div class=\"notification mb-4 is-light is-danger\"><button class=\"delete\" onclick=\"this.parentElement.remove()\"></button>An Error occurred</div>"); "<div class=\"notification mb-4 is-light is-danger\"><button class=\"delete\" onclick=\"this.parentElement.remove()\"></button>An Error occurred</div>");
}) })
</script> </script>
@ -59,4 +59,23 @@
z-index: 6; z-index: 6;
pointer-events: none pointer-events: none
} }
.fade-in {
opacity: 1;
animation-name: fadeInOpacity;
animation-iteration-count: 1;
animation-timing-function: ease-in;
animation-duration: 0.2s;
}
@keyframes fadeInOpacity {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
</style> </style>

View File

@ -1,4 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
You may customize this site by using a custom index page!
{% endblock content %} {% endblock content %}

View File

@ -50,22 +50,22 @@ pub fn create_actix_admin_builder() -> ActixAdminBuilder {
admin_builder.add_custom_handler_for_entity::<AppState, Comment>( admin_builder.add_custom_handler_for_entity::<AppState, Comment>(
"Create Post From Plaintext", "Create Post From Plaintext",
"/create_post_from_plaintext", "/create_post_from_plaintext",
web::post().to(create_post_from_plaintext::<AppState, Comment>)); web::post().to(create_post_from_plaintext::<AppState, Comment>), false);
admin_builder.add_custom_handler_for_entity::<AppState, Post>( admin_builder.add_custom_handler_for_entity::<AppState, Post>(
"Create Post From Plaintext", "Create Post From Plaintext",
"/create_post_from_plaintext", "/create_post_from_plaintext",
web::post().to(create_post_from_plaintext::<AppState, Post>)); web::post().to(create_post_from_plaintext::<AppState, Post>), false);
admin_builder.add_custom_handler_for_entity::<AppState, Post>( admin_builder.add_custom_handler_for_entity::<AppState, Post>(
"Create Post From Plaintext", "Create Post From Plaintext",
"/edit_post_from_plaintext/{id}", "/edit_post_from_plaintext/{id}",
web::post().to(edit_post_from_plaintext::<AppState, Post>)); web::post().to(edit_post_from_plaintext::<AppState, Post>), false);
admin_builder.add_custom_handler_for_entity::<AppState, Comment>( admin_builder.add_custom_handler_for_entity::<AppState, Comment>(
"Create Post From Plaintext", "Create Post From Plaintext",
"/edit_post_from_plaintext/{id}", "/edit_post_from_plaintext/{id}",
web::post().to(edit_post_from_plaintext::<AppState, Comment>)); web::post().to(edit_post_from_plaintext::<AppState, Comment>), false);
admin_builder admin_builder
} }