add custom handlers and add them optionally to the menu
This commit is contained in:
parent
859c139f8b
commit
b39ad99c7f
10
README.md
10
README.md
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
168
src/builder.rs
168
src/builder.rs
@ -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,31 +198,33 @@ 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)
|
||||||
@ -172,6 +234,10 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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>
|
@ -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 %}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user