Compare commits
2 commits
58d1fef4f1
...
f265f217bd
| Author | SHA1 | Date | |
|---|---|---|---|
| f265f217bd | |||
| 3f3634fdab |
22 changed files with 135 additions and 121 deletions
|
|
@ -53,14 +53,14 @@ impl Component for AdminFrame {
|
|||
|
||||
Ok(html! {
|
||||
(breadcrumbs)
|
||||
header.admin-header {
|
||||
h1.admin-page-title { (self.title().using(cx)) }
|
||||
header class="admin-header" {
|
||||
h1 class="admin-page-title" { (self.title().using(cx)) }
|
||||
(local_acts)
|
||||
}
|
||||
@if !local_tasks.0.is_empty() {
|
||||
nav.admin-local-tasks { (local_tasks) }
|
||||
nav class="admin-local-tasks" { (local_tasks) }
|
||||
}
|
||||
div.admin-content {
|
||||
div class="admin-content" {
|
||||
(body)
|
||||
}
|
||||
})
|
||||
|
|
@ -117,7 +117,7 @@ fn render_local_tasks(cx: &Context, current_path: &str) -> Markup {
|
|||
return html! {};
|
||||
}
|
||||
html! {
|
||||
ul.admin-tasks-list {
|
||||
ul class="admin-tasks-list" {
|
||||
@for task in tasks {
|
||||
@let is_active = current_path == task.path;
|
||||
li class=(if is_active { "admin-task admin-task-active" } else { "admin-task" }) {
|
||||
|
|
@ -137,10 +137,12 @@ fn render_local_actions(cx: &Context, current_path: &str) -> Markup {
|
|||
return html! {};
|
||||
}
|
||||
html! {
|
||||
ul.admin-actions-list {
|
||||
ul class="admin-actions-list" {
|
||||
@for action in actions {
|
||||
li.admin-action {
|
||||
a.admin-action-link href=(cx.route(action.url.as_str())) { (action.title.using(cx)) }
|
||||
li class="admin-action" {
|
||||
a class="admin-action-link" href=(cx.route(action.url.as_str())) {
|
||||
(action.title.using(cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,20 +98,20 @@ impl Component for ConfigForm {
|
|||
|
||||
Ok(html! {
|
||||
@if *self.saved() {
|
||||
div.admin-message."admin-message-ok" { (&saved_msg) }
|
||||
div class="admin-message admin-message-ok" { (&saved_msg) }
|
||||
}
|
||||
@if *self.error() {
|
||||
div.admin-message."admin-message-error" { (&error_msg) }
|
||||
div class="admin-message admin-message-error" { (&error_msg) }
|
||||
}
|
||||
form.admin-config-form method="post" action=(action) {
|
||||
form class="admin-config-form" method="post" action=(action) {
|
||||
@for (i, field) in schema.fields().iter().enumerate() {
|
||||
@let pf = &prepared[i];
|
||||
div.admin-form-field {
|
||||
label.admin-form-label for=(field.name()) { (field.label()) }
|
||||
div class="admin-form-field" {
|
||||
label class="admin-form-label" for=(field.name()) { (field.label()) }
|
||||
@match field.field_type() {
|
||||
SettingFieldType::Text { max_length } => {
|
||||
@if let Some(max) = max_length {
|
||||
input.admin-form-input
|
||||
input class="admin-form-input"
|
||||
type="text"
|
||||
id=(field.name())
|
||||
name=(field.name())
|
||||
|
|
@ -119,7 +119,7 @@ impl Component for ConfigForm {
|
|||
maxlength=(max)
|
||||
required[*field.required()];
|
||||
} @else {
|
||||
input.admin-form-input
|
||||
input class="admin-form-input"
|
||||
type="text"
|
||||
id=(field.name())
|
||||
name=(field.name())
|
||||
|
|
@ -129,7 +129,7 @@ impl Component for ConfigForm {
|
|||
}
|
||||
SettingFieldType::Number { min, max } => {
|
||||
@if let (Some(lo), Some(hi)) = (min, max) {
|
||||
input.admin-form-input
|
||||
input class="admin-form-input"
|
||||
type="number"
|
||||
id=(field.name())
|
||||
name=(field.name())
|
||||
|
|
@ -138,7 +138,7 @@ impl Component for ConfigForm {
|
|||
max=(hi)
|
||||
required[*field.required()];
|
||||
} @else if let Some(lo) = min {
|
||||
input.admin-form-input
|
||||
input class="admin-form-input"
|
||||
type="number"
|
||||
id=(field.name())
|
||||
name=(field.name())
|
||||
|
|
@ -146,7 +146,7 @@ impl Component for ConfigForm {
|
|||
min=(lo)
|
||||
required[*field.required()];
|
||||
} @else if let Some(hi) = max {
|
||||
input.admin-form-input
|
||||
input class="admin-form-input"
|
||||
type="number"
|
||||
id=(field.name())
|
||||
name=(field.name())
|
||||
|
|
@ -154,7 +154,7 @@ impl Component for ConfigForm {
|
|||
max=(hi)
|
||||
required[*field.required()];
|
||||
} @else {
|
||||
input.admin-form-input
|
||||
input class="admin-form-input"
|
||||
type="number"
|
||||
id=(field.name())
|
||||
name=(field.name())
|
||||
|
|
@ -163,7 +163,7 @@ impl Component for ConfigForm {
|
|||
}
|
||||
}
|
||||
SettingFieldType::Boolean => {
|
||||
input.admin-form-checkbox
|
||||
input class="admin-form-checkbox"
|
||||
type="checkbox"
|
||||
id=(field.name())
|
||||
name=(field.name())
|
||||
|
|
@ -171,7 +171,7 @@ impl Component for ConfigForm {
|
|||
checked[pf.checked];
|
||||
}
|
||||
SettingFieldType::Select { options } => {
|
||||
select.admin-form-select
|
||||
select class="admin-form-select"
|
||||
id=(field.name())
|
||||
name=(field.name())
|
||||
required[*field.required()]
|
||||
|
|
@ -187,12 +187,12 @@ impl Component for ConfigForm {
|
|||
}
|
||||
}
|
||||
@if let Some(help) = field.help_text() {
|
||||
small.admin-form-help { (help) }
|
||||
small class="admin-form-help" { (help) }
|
||||
}
|
||||
}
|
||||
}
|
||||
div.admin-form-actions {
|
||||
button.admin-btn."admin-btn-primary" type="submit" {
|
||||
div class="admin-form-actions" {
|
||||
button class="admin-btn admin-btn-primary" type="submit" {
|
||||
(&save_label)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,27 +4,25 @@ use pagetop::prelude::*;
|
|||
|
||||
use crate::LOCALES_ADMIN;
|
||||
use crate::component::{AdminFrame, ConfigForm};
|
||||
use crate::registry::{self, AdminPageKind, AdminPermission, AdminSection};
|
||||
use crate::registry::{self, AdminPageKind, AdminSection};
|
||||
use crate::settings;
|
||||
|
||||
// **< Dashboard >**********************************************************************************
|
||||
|
||||
/// GET /admin - Dashboard de administración: lista todas las secciones disponibles.
|
||||
///
|
||||
/// Devuelve [`ErrorPage::AccessDenied`] si el usuario actual no tiene acceso a ninguna página.
|
||||
pub async fn dashboard(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||
let reg = registry::global();
|
||||
let cx = Context::new(request.clone());
|
||||
let sections = reg.ordered_sections();
|
||||
let sections = registry::global().ordered_sections();
|
||||
|
||||
let title = Lc::t("dashboard-title", &LOCALES_ADMIN);
|
||||
let content = render_sections(&cx, §ions);
|
||||
let content = render_sections(&cx, §ions)
|
||||
.ok_or_else(|| ErrorPage::AccessDenied(Some(request.clone())))?;
|
||||
|
||||
Page::admin(request)
|
||||
.with_title(title.clone())
|
||||
.with_child(
|
||||
AdminFrame::new()
|
||||
.with_title(title)
|
||||
.with_child(Html::with(move |_| content.clone())),
|
||||
)
|
||||
.with_child(AdminFrame::new().with_title(title).with_child(content))
|
||||
.render()
|
||||
.await
|
||||
}
|
||||
|
|
@ -37,6 +35,9 @@ pub async fn dashboard(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
|||
/// con ninguna [`AdminPage`](crate::registry::AdminPage) registrada explícitamente por una
|
||||
/// extensión; si una extensión reclama esa ruta con su propia página, esa página gana y este
|
||||
/// handler nunca llega a montarse para ella.
|
||||
///
|
||||
/// Devuelve [`ErrorPage::AccessDenied`] si el usuario actual no tiene acceso a ninguna de sus
|
||||
/// páginas.
|
||||
pub async fn section_page(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
||||
let path = request.path().to_owned();
|
||||
let reg = registry::global();
|
||||
|
|
@ -46,22 +47,14 @@ pub async fn section_page(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
|||
.find(|s| s.path == path)
|
||||
.ok_or_else(|| ErrorPage::NotFound(Some(request.clone())))?;
|
||||
|
||||
require_permission(
|
||||
&request,
|
||||
section.permission.unwrap_or(&AdminPermission::Access),
|
||||
)?;
|
||||
|
||||
let cx = Context::new(request.clone());
|
||||
let title = section.title.clone();
|
||||
let content = render_sections(&cx, &[section]);
|
||||
let content = render_sections(&cx, &[section])
|
||||
.ok_or_else(|| ErrorPage::AccessDenied(Some(request.clone())))?;
|
||||
|
||||
Page::admin(request)
|
||||
.with_title(title.clone())
|
||||
.with_child(
|
||||
AdminFrame::new()
|
||||
.with_title(title)
|
||||
.with_child(Html::with(move |_| content.clone())),
|
||||
)
|
||||
.with_child(AdminFrame::new().with_title(title).with_child(content))
|
||||
.render()
|
||||
.await
|
||||
}
|
||||
|
|
@ -69,41 +62,56 @@ pub async fn section_page(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
|||
// **< Render compartido >**************************************************************************
|
||||
|
||||
// Rejilla de secciones con sus páginas (título + descripción). La usan tanto `dashboard()` (todas
|
||||
// las secciones) como `section_page()` (una sola), filtrando siempre por permiso del usuario actual.
|
||||
fn render_sections(cx: &Context, sections: &[&AdminSection]) -> Markup {
|
||||
// las secciones) como `section_page()` (una sola), filtrando siempre por permiso del usuario
|
||||
// actual. Devuelve `None` si no queda ninguna sección visible con páginas accesibles.
|
||||
fn render_sections(cx: &Context, candidates: &[&'static AdminSection]) -> Option<Container> {
|
||||
let reg = registry::global();
|
||||
html! {
|
||||
div.admin-dashboard-sections {
|
||||
@for section in sections {
|
||||
@if section.is_visible(cx) {
|
||||
@let pages: Vec<_> = reg
|
||||
.pages_for_section(§ion.key)
|
||||
.into_iter()
|
||||
.filter(|p| p.is_accessible(cx))
|
||||
.collect();
|
||||
@if !pages.is_empty() {
|
||||
div.admin-dashboard-section {
|
||||
h2.admin-dashboard-section-title {
|
||||
a href=(cx.route(section.path.as_str())) { (section.title.using(cx)) }
|
||||
}
|
||||
ul.admin-dashboard-section-links {
|
||||
@for page in pages {
|
||||
li.admin-dashboard-link {
|
||||
a href=(cx.route(page.path.as_str())) { (page.title.using(cx)) }
|
||||
@if let Some(description) = &page.description {
|
||||
span.admin-dashboard-link-desc {
|
||||
" - " (description.using(cx))
|
||||
}
|
||||
let sections_with_pages: Vec<_> = candidates
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|section| section.is_visible(cx))
|
||||
.map(|section| {
|
||||
let pages: Vec<_> = reg
|
||||
.pages_for_section(§ion.key)
|
||||
.into_iter()
|
||||
.filter(|p| p.is_accessible(cx))
|
||||
.collect();
|
||||
(section, pages)
|
||||
})
|
||||
.filter(|(_, pages)| !pages.is_empty())
|
||||
.collect();
|
||||
if sections_with_pages.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut sections = Container::new().with_prop(PropsOp::add_classes("admin-dashboard-sections"));
|
||||
for (section, pages) in sections_with_pages {
|
||||
sections = sections.with_child(
|
||||
Container::new()
|
||||
.with_prop(PropsOp::add_classes("admin-dashboard-section"))
|
||||
.with_child(Html::with(move |cx| {
|
||||
html! {
|
||||
h2 class="admin-dashboard-section-title" {
|
||||
a href=(cx.route(section.path.as_str())) { (section.title.using(cx)) }
|
||||
}
|
||||
ul class="admin-dashboard-section-links" {
|
||||
@for page in &pages {
|
||||
li class="admin-dashboard-link" {
|
||||
a href=(cx.route(page.path.as_str())) {
|
||||
(page.title.using(cx))
|
||||
}
|
||||
@if let Some(description) = &page.description {
|
||||
span class="admin-dashboard-link-desc" {
|
||||
" - " (description.using(cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})),
|
||||
);
|
||||
}
|
||||
Some(sections)
|
||||
}
|
||||
|
||||
// **< Config form - GET >**************************************************************************
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
|||
button hx-get="/api/hello" hx-target="#result" {
|
||||
"Say hello"
|
||||
}
|
||||
div #result {}
|
||||
div id="result" {}
|
||||
}))
|
||||
.render().await
|
||||
}
|
||||
|
|
@ -79,7 +79,7 @@ async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
|||
Page::new(request)
|
||||
.with_child(Html::with(move |cx| html! {
|
||||
button (props.unpack(cx)) { "Say hello" }
|
||||
div #result {}
|
||||
div id="result" {}
|
||||
}))
|
||||
.render().await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
|||
button hx-get="/api/hello" hx-target="#result" {
|
||||
"Say hello"
|
||||
}
|
||||
div #result {}
|
||||
div id="result" {}
|
||||
}))
|
||||
.render().await
|
||||
}
|
||||
|
|
@ -80,7 +80,7 @@ async fn homepage(request: HttpRequest) -> Result<Markup, ErrorPage> {
|
|||
Page::new(request)
|
||||
.with_child(Html::with(move |cx| html! {
|
||||
button (props.unpack(cx)) { "Say hello" }
|
||||
div #result {}
|
||||
div id="result" {}
|
||||
}))
|
||||
.render().await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ use crate::hx;
|
|||
/// use pagetop_htmx::prelude::*;
|
||||
///
|
||||
/// async fn add_item() -> impl IntoResponse {
|
||||
/// let new_item = html! { li #item-42 { "New item" } };
|
||||
/// let new_item = html! { li id="item-42" { "New item" } };
|
||||
///
|
||||
/// HtmxResponse::new(new_item)
|
||||
/// .retarget("#list")
|
||||
|
|
@ -123,7 +123,7 @@ impl HtmxResponse {
|
|||
///
|
||||
/// # fn build_response(table: Markup, roles_count: i64) -> HtmxResponse {
|
||||
/// let counter = html! {
|
||||
/// span #roles-count hx-swap-oob="true" { (roles_count) }
|
||||
/// span id="roles-count" hx-swap-oob="true" { (roles_count) }
|
||||
/// };
|
||||
/// HtmxResponse::new(table).oob(counter)
|
||||
/// # }
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ fn header<'a>(response: &'a web::Response, name: &str) -> Option<&'a str> {
|
|||
|
||||
#[pagetop::test]
|
||||
async fn new_renders_the_given_markup_with_an_html_content_type() {
|
||||
let response = HtmxResponse::new(html! { li #item-42 { "New item" } }).into_response();
|
||||
let response = HtmxResponse::new(html! { li id="item-42" { "New item" } }).into_response();
|
||||
|
||||
assert_eq!(
|
||||
header(&response, "content-type"),
|
||||
|
|
@ -41,8 +41,8 @@ async fn empty_has_no_body_but_keeps_the_html_content_type() {
|
|||
|
||||
#[pagetop::test]
|
||||
async fn oob_appends_markup_after_the_main_body() {
|
||||
let response = HtmxResponse::new(html! { li #item-42 { "New item" } })
|
||||
.oob(html! { span #item-count hx-swap-oob="true" { "1" } })
|
||||
let response = HtmxResponse::new(html! { li id="item-42" { "New item" } })
|
||||
.oob(html! { span id="item-count" hx-swap-oob="true" { "1" } })
|
||||
.into_response();
|
||||
|
||||
let body = web::test::read_body_text(response).await;
|
||||
|
|
@ -58,8 +58,8 @@ async fn oob_appends_markup_after_the_main_body() {
|
|||
#[pagetop::test]
|
||||
async fn oob_can_be_called_several_times_to_accumulate_fragments() {
|
||||
let response = HtmxResponse::new(html! { p { "Main" } })
|
||||
.oob(html! { span #a hx-swap-oob="true" { "A" } })
|
||||
.oob(html! { span #b hx-swap-oob="true" { "B" } })
|
||||
.oob(html! { span id="a" hx-swap-oob="true" { "A" } })
|
||||
.oob(html! { span id="b" hx-swap-oob="true" { "B" } })
|
||||
.into_response();
|
||||
|
||||
let body = web::test::read_body_text(response).await;
|
||||
|
|
@ -76,7 +76,7 @@ async fn oob_can_be_called_several_times_to_accumulate_fragments() {
|
|||
#[pagetop::test]
|
||||
async fn oob_works_from_an_empty_response() {
|
||||
let response = HtmxResponse::empty()
|
||||
.oob(html! { span #item-count hx-swap-oob="true" { "0" } })
|
||||
.oob(html! { span id="item-count" hx-swap-oob="true" { "0" } })
|
||||
.into_response();
|
||||
|
||||
let body = web::test::read_body_text(response).await;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ use pagetop::prelude::*;
|
|||
// formularios de autenticación y por los de administración.
|
||||
pub(crate) fn error_banner(error: Option<Lc>) -> Html {
|
||||
Html::with(move |cx| match &error {
|
||||
Some(e) => html! { div.user-form-error role="alert" { (e.clone().using(cx)) } },
|
||||
Some(e) => html! { div class="user-form-error" role="alert" { (e.clone().using(cx)) } },
|
||||
None => html! {},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,13 +111,13 @@ impl Component for RoleTable {
|
|||
|
||||
Ok(html! {
|
||||
div (self.props().unpack(cx)) {
|
||||
div.user-admin-actions {
|
||||
div class="user-admin-actions" {
|
||||
a href=(new_href) {
|
||||
(Lc::t("btn-create-role", &LOCALES_USER).using(cx))
|
||||
}
|
||||
}
|
||||
@if let Some(message) = self.message() {
|
||||
div.user-form-error role="alert" { (message.clone().using(cx)) }
|
||||
div class="user-form-error" role="alert" { (message.clone().using(cx)) }
|
||||
}
|
||||
(table.render(cx).await)
|
||||
(pager)
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ impl Component for UserTable {
|
|||
|
||||
Ok(html! {
|
||||
div (self.props().unpack(cx)) {
|
||||
div.user-admin-actions {
|
||||
div class="user-admin-actions" {
|
||||
a href=(new_href) {
|
||||
(Lc::t("btn-create-user", &LOCALES_USER).using(cx))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,8 +69,8 @@ impl Component for LoginForm {
|
|||
|
||||
let form = form.render(cx).await;
|
||||
Ok(html! {
|
||||
div.user-login-page {
|
||||
div.user-login-card {
|
||||
div class="user-login-page" {
|
||||
div class="user-login-card" {
|
||||
(form)
|
||||
}
|
||||
}
|
||||
|
|
@ -82,13 +82,13 @@ fn links(allow_registration: bool) -> Html {
|
|||
Html::with(move |cx| {
|
||||
html! {
|
||||
@if allow_registration {
|
||||
p.user-register-link {
|
||||
p class="user-register-link" {
|
||||
a href=(cx.route(REGISTER_PATH)) {
|
||||
(Lc::t("link-register", &LOCALES_USER).using(cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
p.user-reset-link {
|
||||
p class="user-reset-link" {
|
||||
a href=(cx.route(PASSWORD_RESET_PATH)) {
|
||||
(Lc::t("link-forgot-password", &LOCALES_USER).using(cx))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ impl Component for UserBlock {
|
|||
Ok(if user.is_authenticated() {
|
||||
let display = user.display_name().unwrap_or("?");
|
||||
html! {
|
||||
nav.user-block {
|
||||
span.user-name { (display) }
|
||||
nav class="user-block" {
|
||||
span class="user-name" { (display) }
|
||||
" · "
|
||||
form.user-logout-inline method="post" action=(cx.route(LOGOUT_PATH)) {
|
||||
form class="user-logout-inline" method="post" action=(cx.route(LOGOUT_PATH)) {
|
||||
button type="submit" {
|
||||
(Lc::t("btn-logout", &LOCALES_USER).using(cx))
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ impl Component for UserBlock {
|
|||
}
|
||||
} else {
|
||||
html! {
|
||||
nav.user-block {
|
||||
nav class="user-block" {
|
||||
a href=(cx.route(LOGIN_PATH)) {
|
||||
(Lc::t("btn-login", &LOCALES_USER).using(cx))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ async fn profile_roles(roles: &[role::Model], cx: &mut Context) -> Block {
|
|||
@if items.is_empty() {
|
||||
"-"
|
||||
} @else {
|
||||
ul.user-profile-roles {
|
||||
ul class="user-profile-roles" {
|
||||
@for (label, system_badge) in &items {
|
||||
li {
|
||||
(label.as_str())
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ pub(crate) fn back_link(href: impl Into<RoutePath>) -> Html {
|
|||
let href = href.into();
|
||||
Html::with(move |cx| {
|
||||
html! {
|
||||
p.user-admin-back-link {
|
||||
p class="user-admin-back-link" {
|
||||
a href=(href.clone()) { (Lc::t("link-back-to-list", &LOCALES_USER).using(cx)) }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,12 +26,12 @@ pub(crate) async fn list_get(request: HttpRequest) -> Result<Response, ErrorPage
|
|||
content = content.with_child(Block::new().with_title(group_label.clone()).with_child(
|
||||
Html::with(move |cx| {
|
||||
html! {
|
||||
table.user-admin-table {
|
||||
table class="user-admin-table" {
|
||||
tbody {
|
||||
@for (key, label) in &items {
|
||||
tr {
|
||||
td { (label.using(cx)) }
|
||||
td.user-admin-permission-key { (key) }
|
||||
td class="user-admin-permission-key" { (key) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -361,12 +361,12 @@ fn role_view_permissions(groups: &PermissionGroups) -> Vec<Block> {
|
|||
.with_title(group_label.clone())
|
||||
.with_child(Html::with(move |cx| {
|
||||
html! {
|
||||
ul.user-admin-permission-list {
|
||||
ul class="user-admin-permission-list" {
|
||||
@for (_key, label, granted) in &perms {
|
||||
@if *granted {
|
||||
li.user-admin-permission-granted { (label.using(cx)) }
|
||||
li class="user-admin-permission-granted" { (label.using(cx)) }
|
||||
} @else {
|
||||
li.user-admin-permission-missing { (label.using(cx)) }
|
||||
li class="user-admin-permission-missing" { (label.using(cx)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ fn search_bar(current_query: Option<String>) -> Html {
|
|||
let value = current_query.unwrap_or_default();
|
||||
Html::with(move |cx| {
|
||||
html! {
|
||||
div.user-admin-search {
|
||||
div class="user-admin-search" {
|
||||
input
|
||||
type="search"
|
||||
id="user-admin-search-input"
|
||||
|
|
@ -588,7 +588,7 @@ async fn user_view_roles(roles: &[role::Model], cx: &mut Context) -> Block {
|
|||
@if items.is_empty() {
|
||||
"-"
|
||||
} @else {
|
||||
table.user-admin-table {
|
||||
table class="user-admin-table" {
|
||||
tbody {
|
||||
@for (id, machine_name, label, system_badge) in &items {
|
||||
@let href = cx.route(role_path(*id, "view")).to_string();
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ impl Component for Breadcrumb {
|
|||
|
||||
Ok(html! {
|
||||
nav (self.props().unpack(cx)) aria-label=[Lc::l("breadcrumb_label").lookup(cx)] {
|
||||
ol.breadcrumb {
|
||||
ol class="breadcrumb" {
|
||||
@for crumb in self.crumbs() {
|
||||
(crumb.render_crumb(cx))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ impl Component for Template {
|
|||
}
|
||||
|
||||
Ok(html! {
|
||||
div.wrapper {
|
||||
div class="wrapper" {
|
||||
(body)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -240,7 +240,7 @@ impl Component for Pager {
|
|||
@let items_per_page = self.items_per_page().max(1);
|
||||
@let first = (page - 1) * items_per_page + 1;
|
||||
@let last = (page * items_per_page).min(self.total_items());
|
||||
span.pager-summary {
|
||||
span class="pager-summary" {
|
||||
(Lc::l("pager_summary")
|
||||
.with_arg("first", first.to_string())
|
||||
.with_arg("last", last.to_string())
|
||||
|
|
@ -248,14 +248,16 @@ impl Component for Pager {
|
|||
.using(cx))
|
||||
}
|
||||
}
|
||||
ul.pagination {
|
||||
ul class="pagination" {
|
||||
@if show_prev_next {
|
||||
li.page-item.page-previous.disabled[first_disabled] {
|
||||
a.page-link
|
||||
a class="page-link"
|
||||
href=[(!first_disabled).then(|| Self::page_route(&route, page - 1))]
|
||||
aria-disabled=[first_disabled.then_some("true")]
|
||||
aria-label=[Lc::l("pager_previous_aria_label").lookup(cx)] {
|
||||
span.page-link-icon { (Lc::l("pager_previous_label").using(cx)) }
|
||||
span class="page-link-icon" {
|
||||
(Lc::l("pager_previous_label").using(cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -264,7 +266,7 @@ impl Component for Pager {
|
|||
PageItem::Number(n) => {
|
||||
@let href = Self::page_route(&route, n);
|
||||
li.page-item.active[n == page] {
|
||||
a.page-link
|
||||
a class="page-link"
|
||||
href=(href)
|
||||
aria-current=[(n == page).then_some("page")] {
|
||||
(n.to_string())
|
||||
|
|
@ -272,19 +274,21 @@ impl Component for Pager {
|
|||
}
|
||||
}
|
||||
PageItem::Ellipsis => {
|
||||
li.page-item.page-ellipsis.disabled aria-hidden="true" {
|
||||
span.page-link { "…" }
|
||||
li class="page-item page-ellipsis disabled" aria-hidden="true" {
|
||||
span class="page-link" { "…" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@if show_prev_next {
|
||||
li.page-item.page-next.disabled[last_disabled] {
|
||||
a.page-link
|
||||
a class="page-link"
|
||||
href=[(!last_disabled).then(|| Self::page_route(&route, page + 1))]
|
||||
aria-disabled=[last_disabled.then_some("true")]
|
||||
aria-label=[Lc::l("pager_next_aria_label").lookup(cx)] {
|
||||
span.page-link-icon { (Lc::l("pager_next_label").using(cx)) }
|
||||
span class="page-link-icon" {
|
||||
(Lc::l("pager_next_label").using(cx))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ impl Component for Table {
|
|||
let colspan = self.columns().len().max(1).to_string();
|
||||
|
||||
Ok(html! {
|
||||
div.table-responsive {
|
||||
div class="table-responsive" {
|
||||
table (self.props().unpack(cx)) {
|
||||
@if !self.columns().is_empty() {
|
||||
thead {
|
||||
|
|
@ -108,7 +108,7 @@ impl Component for Table {
|
|||
{
|
||||
tbody {
|
||||
tr {
|
||||
td.table-empty colspan=(colspan) { (empty) }
|
||||
td class="table-empty" colspan=(colspan) { (empty) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -312,8 +312,8 @@ pub trait Theme: Extension + Send + Sync {
|
|||
/// cx: &mut Context,
|
||||
/// ) -> Option<Result<Markup, ComponentError>> {
|
||||
/// render_component!(component, {
|
||||
/// Button => |btn| Ok(html! { button.btn.btn-primary { (btn.label()) } }),
|
||||
/// Heading => |h| Ok(html! { h2.display-4 { (h.text()) } }),
|
||||
/// Button => |btn| Ok(html! { button class="btn btn-primary" { (btn.label()) } }),
|
||||
/// Heading => |h| Ok(html! { h2 class="display-4" { (h.text()) } }),
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
|
|
@ -487,13 +487,13 @@ macro_rules! setup_component {
|
|||
/// cx: &mut Context,
|
||||
/// ) -> Option<Result<Markup, ComponentError>> {
|
||||
/// render_component!(component, {
|
||||
/// Button => |btn| { Ok(html! { button.btn.btn-primary { (btn.label()) } }) },
|
||||
/// Button => |btn| { Ok(html! { button class="btn btn-primary" { (btn.label()) } }) },
|
||||
/// Heading => |h| self.render_heading(h, cx),
|
||||
/// })
|
||||
/// }
|
||||
///
|
||||
/// fn render_heading(&self, h: &Heading, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||
/// Ok(html! { h2.display-4 { (h.text()) } })
|
||||
/// Ok(html! { h2 class="display-4" { (h.text()) } })
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue