diff --git a/assets/scss/basic/_user.scss b/assets/scss/basic/_user.scss index 38e4d094..877b56a1 100644 --- a/assets/scss/basic/_user.scss +++ b/assets/scss/basic/_user.scss @@ -38,19 +38,6 @@ font-size: 0.9375rem; } -.user-admin-permission-list { - list-style: none; - padding-left: 0; -} - -.user-admin-permission-granted { - font-weight: 600; -} - -.user-admin-permission-missing { - color: var(--val-color--text--muted); -} - .user-form-error { padding: 0.625rem 0.875rem; margin-bottom: 1rem; diff --git a/extensions/pagetop-user/src/component/admin/role_table.rs b/extensions/pagetop-user/src/component/admin/role_table.rs index aaa6c68d..79ae47f4 100644 --- a/extensions/pagetop-user/src/component/admin/role_table.rs +++ b/extensions/pagetop-user/src/component/admin/role_table.rs @@ -88,6 +88,15 @@ impl Component for RoleTable { } else { None }; + let no_permissions_badge = if role.locked || role.has_permissions { + None + } else { + Some( + Badge::neutral(Lc::t("badge-no-permissions", &LOCALES_USER)) + .render(cx) + .await, + ) + }; table.alter_row( table::Row::new() @@ -96,6 +105,7 @@ impl Component for RoleTable { .with_cell(Html::with(move |_cx| { html! { @if let Some(badge) = &system_badge { (badge) } + @if let Some(badge) = &no_permissions_badge { (badge) } } })) .with_cell(role.user_count.to_string()) @@ -166,10 +176,9 @@ impl RoleTable { self } - // URL del listado con el estado actual (orden, página): es el valor que viaja como - // `waypoint` en los enlaces de ver/editar/permisos, para poder volver exactamente a este - // mismo estado. Se construye con `cx.route()` para que preserve el parámetro `lang` cuando - // corresponda. + // URL del listado con el estado actual (orden, página): es el valor que viaja como `waypoint` + // en los enlaces de ver/editar/permisos, para poder volver exactamente a este mismo estado. Se + // construye con `cx.route()` para que preserve el parámetro `lang` cuando corresponda. fn list_href(&self, cx: &Context) -> String { cx.route(ADMIN_ROLES_PATH) .alter_param("sort", self.sort().as_str()) @@ -219,8 +228,8 @@ fn label_cell(role: &RoleListItem, waypoint: &Waypoint) -> Html { } // Construye la celda de acciones: gestionar permisos siempre, y editar/borrar sólo si el rol no -// está bloqueado por el sistema. Devuelve un componente `Html` para que el marcado se genere -// cuando `Table` renderice la celda, no al construir la fila. +// está bloqueado por 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( role: &RoleListItem, waypoint: &Waypoint, @@ -235,10 +244,10 @@ async fn actions_cell( let permissions_href = waypoint.append_to(cx.route(role_path(id, "permissions"))); - // Los botones se renderizan aquí, no dentro del `Html::with()` de abajo: necesitan pasar por - // su propio ciclo de renderizado (`.render().await`) para que el tema activo los estilice - // igual (ver `pagetop-bootsier::theme::bs::button`), incluida la traducción de `data-dialog-*` - // que usa el botón de borrado. + // Los botones se renderizan aquí, no dentro del `Html::with()` de abajo: necesitan pasar por su + // propio ciclo de renderizado (`.render().await`) para que el tema activo los estilice igual + // (ver `pagetop-bootsier::theme::bs::button`), incluida la traducción de `data-dialog-*` que + // usa el botón de borrado. let permissions_button = Button::anchor( Lc::t("btn-manage-permissions", &LOCALES_USER), permissions_href, @@ -258,8 +267,8 @@ async fn actions_cell( .render(cx) .await; - // Viaja como query string para que, tanto si el borrado falla como si tiene éxito, la - // tabla vuelva a mostrarse en la misma página/orden en que estaba, en vez de reiniciarse. + // Viaja como query string para que, tanto si el borrado falla como si tiene éxito, la tabla + // vuelva a mostrarse en la misma página/orden en que estaba, en vez de reiniciarse. let confirm_href = cx .route(role_path(id, "delete/confirm")) .alter_param("sort", sort.as_str()) diff --git a/extensions/pagetop-user/src/handlers/admin/roles.rs b/extensions/pagetop-user/src/handlers/admin/roles.rs index 023621a4..0dc5032c 100644 --- a/extensions/pagetop-user/src/handlers/admin/roles.rs +++ b/extensions/pagetop-user/src/handlers/admin/roles.rs @@ -348,31 +348,33 @@ async fn role_view_details(role: &role::Model, cx: &mut Context) -> Block { .with_child(table) } -// Un bloque por grupo del catálogo de permisos: cada permiso concedido se marca con la clase -// `user-admin-permission-granted` (negrita, vía CSS del tema); el resto con -// `user-admin-permission-missing` (gris claro, vía CSS del tema). +// Un bloque por grupo del catálogo con los permisos concedidos al rol; los grupos sin ninguno no se +// muestran. Si el rol no tiene ningún permiso, un único bloque lo indica. fn role_view_permissions(groups: &PermissionGroups) -> Vec { - groups + let blocks: Vec = groups .iter() + .filter(|(_, perms)| perms.iter().any(|(_, _, granted)| *granted)) .map(|(group_label, perms)| { - let perms = perms.clone(); + let mut table = Table::new().with_prop(PropsOp::add_classes("user-admin-table")); + for (_key, label, _) in perms.iter().filter(|(_, _, granted)| *granted) { + table = table.with_row(table::Row::new().with_cell(label.clone())); + } Block::new() .with_title(group_label.clone()) - .with_child(Html::with(move |cx| { - html! { - ul class="user-admin-permission-list" { - @for (_key, label, granted) in &perms { - @if *granted { - li class="user-admin-permission-granted" { (label.using(cx)) } - } @else { - li class="user-admin-permission-missing" { (label.using(cx)) } - } - } - } - } - })) + .with_child(table) }) - .collect() + .collect(); + + if blocks.is_empty() { + return vec![ + Block::new() + .with_title(Lc::t("title-admin-permissions", &LOCALES_USER)) + .with_child( + Table::new().with_empty(Lc::t("empty-role-permissions", &LOCALES_USER)), + ), + ]; + } + blocks } // **< delete_post >******************************************************************************** @@ -537,8 +539,8 @@ pub(crate) struct RolePermissionsFormData { /// POST /admin/user/roles/{id}/permissions - Reemplaza el conjunto de permisos de un rol. /// -/// Usa `RawForm` + `serde_qs` en lugar de `axum::extract::Form` (basado en `serde_urlencoded`, -/// que no deserializa claves repetidas como `permission_keys=a&permission_keys=b` en un `Vec`). +/// Usa `RawForm` + `serde_qs` en lugar de `axum::extract::Form` (basado en `serde_urlencoded`, que +/// no deserializa claves repetidas como `permission_keys=a&permission_keys=b` en un `Vec`). pub(crate) async fn permissions_post( request: HttpRequest, web::Path(id): web::Path, diff --git a/extensions/pagetop-user/src/locale/en-US/common.ftl b/extensions/pagetop-user/src/locale/en-US/common.ftl index e6220586..8467f8a4 100644 --- a/extensions/pagetop-user/src/locale/en-US/common.ftl +++ b/extensions/pagetop-user/src/locale/en-US/common.ftl @@ -140,14 +140,16 @@ link-back-to-list = Back to list # **< Admin: confirmations and badges >** -confirm-delete-role = Delete this role? This cannot be undone. -confirm-change-status = Change this account's status? -confirm-grant-admin = Grant unrestricted access to this account? -confirm-revoke-admin = Revoke this account's unrestricted access? -badge-system-role = System -badge-admin = Administrator -empty-users-list = No users found. -empty-roles-list = No roles found. +confirm-delete-role = Delete this role? This cannot be undone. +confirm-change-status = Change this account's status? +confirm-grant-admin = Grant unrestricted access to this account? +confirm-revoke-admin = Revoke this account's unrestricted access? +badge-system-role = System +badge-admin = Administrator +badge-no-permissions = No permissions +empty-users-list = No users found. +empty-roles-list = No roles found. +empty-role-permissions = This role has no permissions. # **< Admin: error messages >** diff --git a/extensions/pagetop-user/src/locale/es-ES/common.ftl b/extensions/pagetop-user/src/locale/es-ES/common.ftl index a010099e..3b709b0c 100644 --- a/extensions/pagetop-user/src/locale/es-ES/common.ftl +++ b/extensions/pagetop-user/src/locale/es-ES/common.ftl @@ -140,14 +140,16 @@ link-back-to-list = Volver al listado # **< Administración: confirmaciones y distintivos >** -confirm-delete-role = ¿Eliminar este rol? Esta acción no se puede deshacer. -confirm-change-status = ¿Cambiar el estado de esta cuenta? -confirm-grant-admin = ¿Conceder acceso irrestricto a esta cuenta? -confirm-revoke-admin = ¿Revocar el acceso irrestricto de esta cuenta? -badge-system-role = Sistema -badge-admin = Administrador -empty-users-list = No se han encontrado usuarios. -empty-roles-list = No se han encontrado roles. +confirm-delete-role = ¿Eliminar este rol? Esta acción no se puede deshacer. +confirm-change-status = ¿Cambiar el estado de esta cuenta? +confirm-grant-admin = ¿Conceder acceso irrestricto a esta cuenta? +confirm-revoke-admin = ¿Revocar el acceso irrestricto de esta cuenta? +badge-system-role = Sistema +badge-admin = Administrador +badge-no-permissions = Sin permisos +empty-users-list = No se han encontrado usuarios. +empty-roles-list = No se han encontrado roles. +empty-role-permissions = Este rol no tiene permisos. # **< Administración: mensajes de error >** diff --git a/extensions/pagetop-user/src/service/role_admin.rs b/extensions/pagetop-user/src/service/role_admin.rs index 569b080e..6d6fdbab 100644 --- a/extensions/pagetop-user/src/service/role_admin.rs +++ b/extensions/pagetop-user/src/service/role_admin.rs @@ -1,6 +1,6 @@ //! Servicio de administración de roles: listado, CRUD y permisos. -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use pagetop::prelude::*; use pagetop_seaorm::db::{ @@ -47,6 +47,7 @@ pub(crate) struct RoleListItem { pub label: String, pub locked: bool, pub user_count: u64, + pub has_permissions: bool, } pub(crate) struct RoleListParams { @@ -104,7 +105,7 @@ pub(crate) async fn list_roles_page( async fn role_items(roles: Vec) -> Result, AuthError> { let role_ids: Vec = roles.iter().map(|r| r.id).collect(); let counts: Vec<(i32, i64)> = user_role::Entity::find() - .filter(user_role::Column::RoleId.is_in(role_ids)) + .filter(user_role::Column::RoleId.is_in(role_ids.clone())) .select_only() .column(user_role::Column::RoleId) .column_as(user_role::Column::RoleId.count(), "count") @@ -116,11 +117,22 @@ async fn role_items(roles: Vec) -> Result, AuthEr .into_iter() .map(|(role_id, count)| (role_id, count as u64)) .collect(); + let with_permissions: HashSet = role_permission::Entity::find() + .filter(role_permission::Column::RoleId.is_in(role_ids)) + .select_only() + .column(role_permission::Column::RoleId) + .distinct() + .into_tuple() + .all(dbconn()) + .await? + .into_iter() + .collect(); Ok(roles .into_iter() .map(|role| RoleListItem { user_count: counts_by_role.get(&role.id).copied().unwrap_or(0), + has_permissions: with_permissions.contains(&role.id), id: role.id, machine_name: role.machine_name, label: role.label,