✨ (user): Deshabilita roles si no hay asignables

This commit is contained in:
Manuel Cillero 2026-10-02 23:48:18 +02:00
parent 56a2471090
commit b5da11777a
2 changed files with 28 additions and 6 deletions

View file

@ -7,6 +7,7 @@ use pagetop_htmx::hx_table::sort_link;
use crate::ADMIN_USERS_PATH;
use crate::LOCALES_USER;
use crate::account::UserStatus;
use crate::handlers::admin::users::available_roles;
use crate::permission::UserPermission;
use crate::service::user_admin::{UserListItem, UserSortField};
use crate::user_path;
@ -68,6 +69,11 @@ impl Component for UserTable {
let can_assign_roles = cx
.request()
.is_some_and(|r| has_permission(r, &UserPermission::AssignRoles));
// Sólo hace falta consultar si hay algún rol asignable cuando el botón vaya a mostrarse.
let has_assignable_roles = can_assign_roles
&& available_roles(&[])
.await
.is_ok_and(|roles| !roles.is_empty());
for user in self.items() {
let status = user.status;
@ -79,7 +85,10 @@ impl Component for UserTable {
.with_cell(user.display_name.as_deref().unwrap_or("-"))
.with_cell(roles_cell(user, cx).await)
.with_cell(Lc::t(status_key(status), &LOCALES_USER))
.with_cell(actions_cell(user, &waypoint, can_assign_roles, cx).await),
.with_cell(
actions_cell(user, &waypoint, can_assign_roles, has_assignable_roles, cx)
.await,
),
);
}
@ -203,12 +212,14 @@ fn username_cell(user: &UserListItem, waypoint: &Waypoint) -> Html {
}
// Construye la celda de acciones: editar siempre, y gestionar roles sólo si el usuario autenticado
// tiene permiso para asignarlos. Devuelve un componente `Html` para que el marcado se genere
// cuando `Table` renderice la celda, no al construir la fila.
// tiene permiso para asignarlos; en ese caso, deshabilitado si no hay ningún rol asignable en todo
// el sistema. Devuelve un componente `Html` para que el marcado se genere cuando `Table` renderice
// la celda, no al construir la fila.
async fn actions_cell(
user: &UserListItem,
waypoint: &Waypoint,
can_assign_roles: bool,
has_assignable_roles: bool,
cx: &mut Context,
) -> Html {
let id = user.id;
@ -229,6 +240,7 @@ async fn actions_cell(
Button::anchor(Lc::t("btn-manage-roles", &LOCALES_USER), roles_href)
.with_style(button::Style::Solid(Intent::Neutral))
.with_size(button::Size::Small)
.with_disabled(!has_assignable_roles)
.render(cx)
.await,
)

View file

@ -112,8 +112,11 @@ fn search_bar(current_query: Option<String>) -> Html {
// **< available_roles >****************************************************************************
// Roles asignables desde la UI de usuarios: excluye "anonymous" (nunca se asigna explícitamente)
// y "authenticated" (implícito, nunca se asigna).
async fn available_roles(selected: &[i32]) -> Result<Vec<(i32, String, bool)>, AuthError> {
// y "authenticated" (implícito, nunca se asigna). `pub(crate)` porque también la usa
// `component::admin::user_table` para decidir si hay algo que gestionar.
pub(crate) async fn available_roles(
selected: &[i32],
) -> Result<Vec<(i32, String, bool)>, AuthError> {
let items = role_admin::list_roles(&role_admin::RoleListParams {
sort: role_admin::RoleSortField::Weight,
dir: SortDir::Asc,
@ -276,6 +279,10 @@ async fn render_user_edit(
let can_toggle_admin = request
.extension::<Account>()
.is_some_and(|a| a.is_admin && a.id != id);
let has_assignable_roles = match available_roles(&[]).await {
Ok(roles) => !roles.is_empty(),
Err(_) => return ErrorPage::InternalError(Some(request)).into_response(),
};
let mut page = Page::admin(request);
let back_href = waypoint.or(page.context().route(ADMIN_USERS_PATH));
let title = Lc::t("title-admin-user-edit", &LOCALES_USER);
@ -284,6 +291,7 @@ async fn render_user_edit(
status,
user.is_admin,
can_toggle_admin,
has_assignable_roles,
&waypoint,
page.context(),
);
@ -328,6 +336,7 @@ fn edit_actions(
status: UserStatus,
target_is_admin: bool,
can_toggle_admin: bool,
has_assignable_roles: bool,
waypoint: &Waypoint,
cx: &mut Context,
) -> Flex {
@ -370,7 +379,8 @@ fn edit_actions(
)
.with_child(
Button::anchor(Lc::t("btn-manage-roles", &LOCALES_USER), roles_href)
.with_style(button::Style::Solid(Intent::Neutral)),
.with_style(button::Style::Solid(Intent::Neutral))
.with_disabled(!has_assignable_roles),
)
.with_child(
Button::anchor(Lc::t("btn-reset-password", &LOCALES_USER), password_href)