Compare commits

..

No commits in common. "b5da11777a081051ea000f12f3adae0156db194c" and "c06efbd0d1c6fe916fac137ab79b98ba210dbda3" have entirely different histories.

5 changed files with 8 additions and 58 deletions

View file

@ -162,11 +162,9 @@ body {
background-color: color-mix(in srgb, var(--val-color--primary) 85%, black); background-color: color-mix(in srgb, var(--val-color--primary) 85%, black);
border-color: color-mix(in srgb, var(--val-color--primary) 85%, black); border-color: color-mix(in srgb, var(--val-color--primary) 85%, black);
} }
.button:disabled, .button:disabled {
.button.disabled {
opacity: 0.65; opacity: 0.65;
cursor: default; cursor: default;
pointer-events: none;
} }
.button::-moz-focus-inner { .button::-moz-focus-inner {
border: 0; border: 0;
@ -239,8 +237,7 @@ body {
background-color: transparent; background-color: transparent;
border-color: transparent; border-color: transparent;
} }
.button-link:disabled, .button-link:disabled {
.button-link.disabled {
color: var(--val-color--text--muted); color: var(--val-color--text--muted);
} }

View file

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

View file

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

View file

@ -92,12 +92,6 @@ impl Component for Button {
Style::Outline(intent) => util::join!("button button-outline-", intent.color(cx)), Style::Outline(intent) => util::join!("button button-outline-", intent.color(cx)),
Style::Link => "button button-link".to_string(), Style::Link => "button button-link".to_string(),
})); }));
// Un `<button disabled>` es deshabilitado por el navegador (`:disabled`), pero un `<a>` sin
// `href` (ver `prepare()`) no tiene un estado nativo similar. Por eso la clase `disabled`.
if *self.disabled() {
self.alter_prop(PropsOp::add_classes("disabled"));
}
} }
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> { async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {

View file

@ -142,22 +142,3 @@ async fn disabled_anchor_omits_href_and_sets_aria_disabled() {
assert!(html.contains(r#"aria-disabled="true""#)); assert!(html.contains(r#"aria-disabled="true""#));
assert!(html.contains(r#"tabindex="-1""#)); assert!(html.contains(r#"tabindex="-1""#));
} }
// A disabled `<a>` has no native equivalent to `:disabled` that CSS can hook into, unlike a
// `<button disabled>`; the `disabled` class is what both Basic (`.button.disabled`) and Bootsier
// (Bootstrap's own `.btn.disabled`) actually style.
#[pagetop::test]
async fn disabled_anchor_gets_the_disabled_class() {
let mut button = Button::anchor(Lc::n("Edit"), "/items/1/edit").with_disabled(true);
let html = button.render(&mut Context::default()).await.into_string();
assert!(html.contains(r#"class="button disabled""#));
}
#[pagetop::test]
async fn disabled_button_also_gets_the_disabled_class() {
let mut button = Button::submit(Lc::n("Save")).with_disabled(true);
let html = button.render(&mut Context::default()).await.into_string();
assert!(html.contains(r#"class="button disabled""#));
}