Compare commits
2 commits
c06efbd0d1
...
b5da11777a
| Author | SHA1 | Date | |
|---|---|---|---|
| b5da11777a | |||
| 56a2471090 |
5 changed files with 58 additions and 8 deletions
|
|
@ -162,9 +162,11 @@ 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;
|
||||||
|
|
@ -237,7 +239,8 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ 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;
|
||||||
|
|
@ -68,6 +69,11 @@ 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;
|
||||||
|
|
@ -79,7 +85,10 @@ 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(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
|
// 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
|
// tiene permiso para asignarlos; en ese caso, deshabilitado si no hay ningún rol asignable en todo
|
||||||
// cuando `Table` renderice la celda, no al construir la fila.
|
// 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(
|
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;
|
||||||
|
|
@ -229,6 +240,7 @@ 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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -112,8 +112,11 @@ 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).
|
// y "authenticated" (implícito, nunca se asigna). `pub(crate)` porque también la usa
|
||||||
async fn available_roles(selected: &[i32]) -> Result<Vec<(i32, String, bool)>, AuthError> {
|
// `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 {
|
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,
|
||||||
|
|
@ -276,6 +279,10 @@ 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);
|
||||||
|
|
@ -284,6 +291,7 @@ 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(),
|
||||||
);
|
);
|
||||||
|
|
@ -328,6 +336,7 @@ 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 {
|
||||||
|
|
@ -370,7 +379,8 @@ 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)
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,12 @@ 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> {
|
||||||
|
|
|
||||||
|
|
@ -142,3 +142,22 @@ 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""#));
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue