✨ (user): Ordena alfabéticamente grupos de permisos

Añade `Lc::collation_key()` en PageTop para obtener una clave de
ordenación no sensible a mayúsculas y acentos, con `ñ` como letra propia
entre `n` y `o` en español.
This commit is contained in:
Manuel Cillero 2026-09-27 13:12:57 +02:00
parent 93cd3ba6c5
commit dbdaeb6151
7 changed files with 144 additions and 18 deletions

View file

@ -14,11 +14,13 @@ use crate::permission::{self, UserPermission};
pub(crate) async fn list_get(request: HttpRequest) -> Result<Response, ErrorPage> {
require_permission(&request, &UserPermission::AdminPermissions)?;
let mut page = Page::admin(request);
let registry = permission::registry();
let title = Lc::t("title-admin-permissions", &LOCALES_USER);
let mut content = frame(title.clone());
for (group, group_label) in registry.groups() {
for (group, group_label) in registry.groups_sorted(page.context()) {
let items: Vec<(CowStr, Lc)> = registry
.by_group(group)
.map(|permission| (permission.key(), permission.label()))
@ -41,6 +43,10 @@ pub(crate) async fn list_get(request: HttpRequest) -> Result<Response, ErrorPage
));
}
let mut page = Page::admin(request).with_title(title).with_child(content);
Ok(page.render().await.into_response())
Ok(page
.with_title(title)
.with_child(content)
.render()
.await
.into_response())
}

View file

@ -157,7 +157,7 @@ pub(crate) async fn new_post(
}
}
// **< edit_get / edit_post >************************************************************************
// **< edit_get / edit_post >***********************************************************************
/// GET /admin/user/roles/{id}/edit - Formulario de edición de rol.
pub(crate) async fn edit_get(
@ -286,9 +286,9 @@ pub(crate) async fn view_get(
Ok(keys) => keys,
Err(_) => return Err(ErrorPage::InternalError(Some(request))),
};
let groups = build_permission_groups(&selected);
let mut page = Page::admin(request);
let groups = build_permission_groups(&selected, page.context());
let back_href = waypoint.or(page.context().route(ADMIN_ROLES_PATH));
let details_block = role_view_details(&role, page.context()).await;
@ -469,13 +469,13 @@ pub(crate) async fn delete_confirm_get(
Ok(HtmxResponse::new(button.render(&mut cx).await).into_response())
}
// **< permissions_get / permissions_post >**********************************************************
// **< permissions_get / permissions_post >*********************************************************
fn build_permission_groups(selected: &[String]) -> PermissionGroups {
fn build_permission_groups(selected: &[String], cx: &Context) -> PermissionGroups {
let registry = permission::registry();
registry
.groups()
.iter()
.groups_sorted(cx)
.into_iter()
.map(|(group, group_label)| {
let perms = registry
.by_group(group)
@ -507,9 +507,9 @@ pub(crate) async fn permissions_get(
Ok(keys) => keys,
Err(_) => return Err(ErrorPage::InternalError(Some(request))),
};
let groups = build_permission_groups(&selected);
let mut page = Page::admin(request);
let groups = build_permission_groups(&selected, page.context());
let back_href = waypoint.or(page.context().route(ADMIN_ROLES_PATH));
let title = Lc::t("title-admin-role-permissions", &LOCALES_USER);
@ -560,8 +560,9 @@ pub(crate) async fn permissions_post(
Ok(Redirect::see_other(target).into_response())
}
Err(err) => {
let groups = build_permission_groups(&form.permission_keys);
let mut page = Page::admin(request);
let groups = build_permission_groups(&form.permission_keys, page.context());
let back_href = waypoint.or(page.context().route(ADMIN_ROLES_PATH));
let form_component = RolePermissionsForm::new()
.with_role_id(id)

View file

@ -2,7 +2,7 @@
# **< Group: Users >**
group-users = Users
group-users = User management
perm-login = Sign in
perm-register = Register a new account

View file

@ -2,7 +2,7 @@
# **< Grupo: Usuarios >**
group-users = Usuarios
group-users = Gestión de usuarios
perm-login = Iniciar sesión
perm-register = Registrar una cuenta nueva

View file

@ -68,9 +68,10 @@ impl ActionDispatcher for DeclarePermissions {}
/// Catálogo mutable de permisos, construido durante la fase de inicialización.
///
/// Un `Vec` basta: el catálogo se construye una sola vez con un puñado de entradas y se recorre
/// entero en la UI de administración, así que conserva el orden de registro sin estructuras
/// adicionales y sin el coste de mantenerlas sincronizadas.
/// Se construye una sola vez con un conjunto de entradas que conserva el orden de registro sin
/// estructuras adicionales. La UI de administración no usa ese orden de registro. Llama a
/// [`groups_sorted()`](Self::groups_sorted), que lo reordena alfabéticamente para cada petición
/// según el idioma activo.
#[derive(Default)]
pub struct PermissionRegistry {
permissions: Vec<PermissionRef>,
@ -105,10 +106,28 @@ impl PermissionRegistry {
self.permissions.iter().copied()
}
/// Grupos en orden de primer registro, sin traducir ni ordenar alfabéticamente.
///
/// Listado de bajo nivel sobre el que se apoya [`groups_sorted()`](Self::groups_sorted); úsalo
/// en su lugar sólo cuando el orden de registro sea justo lo que se necesita (por ejemplo, para
/// inspeccionar el catálogo en pruebas).
pub fn groups(&self) -> &[(&'static str, Lc)] {
&self.groups
}
/// Grupos ordenados alfabéticamente por su título traducido al idioma del contexto, no sensible
/// a mayúsculas y, en general, a los acentos (ver [`Lc::collation_key()`] para el criterio
/// exacto y sus límites).
pub fn groups_sorted(&self, cx: &Context) -> Vec<&(&'static str, Lc)> {
let mut groups: Vec<_> = self
.groups
.iter()
.map(|g| (g.1.collation_key(cx), g))
.collect();
groups.sort_by(|a, b| a.0.cmp(&b.0));
groups.into_iter().map(|(_, g)| g).collect()
}
pub fn by_group<'a>(&'a self, group: &'a str) -> impl Iterator<Item = PermissionRef> + 'a {
self.permissions
.iter()