feat(examples): add saas settings page fallback

Make the SaaS tutorial's settings route render a distinct full-page fallback while the enhanced handler still swaps the generated page panel and pushes history. This proves page-swap shape without relying only on the enhanced interaction path.

req: examples/001

req: page_swap/002
This commit is contained in:
slhx agent
2026-06-05 10:19:30 +02:00
parent a428097e0e
commit 5ab5846b01
3 changed files with 74 additions and 21 deletions
+52 -4
View File
@@ -181,6 +181,8 @@ pub struct Dashboard {
summary: String, summary: String,
rows: Vec<ProjectRow>, rows: Vec<ProjectRow>,
project_count: usize, project_count: usize,
show_projects: bool,
settings: SettingsPage,
} }
impl Dashboard { impl Dashboard {
@@ -191,9 +193,18 @@ impl Dashboard {
flash: "Signed in with a demo session".to_owned(), flash: "Signed in with a demo session".to_owned(),
summary: project_summary(projects.len()), summary: project_summary(projects.len()),
project_count: projects.len(), project_count: projects.len(),
show_projects: true,
settings: SettingsPage::production_boundaries(),
rows: projects.into_iter().map(ProjectRow::from).collect(), rows: projects.into_iter().map(ProjectRow::from).collect(),
} }
} }
pub fn settings(ctx: &AppContext) -> Self {
let mut dashboard = Self::from_context(ctx);
dashboard.show_projects = false;
dashboard.flash.clear();
dashboard
}
} }
#[derive(Hemplate)] #[derive(Hemplate)]
@@ -226,6 +237,14 @@ pub struct SettingsPage {
message: &'static str, message: &'static str,
} }
impl SettingsPage {
fn production_boundaries() -> Self {
Self {
message: "Auth, CSRF, persistence, metrics, and deploy stay explicit app integrations.",
}
}
}
#[derive(Hemplate)] #[derive(Hemplate)]
pub struct AppShell { pub struct AppShell {
title: &'static str, title: &'static str,
@@ -239,6 +258,13 @@ pub fn home_page(ctx: &AppContext) -> Html {
}) })
} }
pub fn settings_page(ctx: &AppContext) -> Html {
hemx::page(&AppShell {
title: "hemx SaaS tutorial settings",
body: hemx::page(&Dashboard::settings(ctx)),
})
}
#[hemx::app(dashboard_handlers)] #[hemx::app(dashboard_handlers)]
pub fn registry(ctx: AppContext) -> Registry { pub fn registry(ctx: AppContext) -> Registry {
interactions(ui::BUILD_FINGERPRINT) interactions(ui::BUILD_FINGERPRINT)
@@ -277,10 +303,7 @@ mod dashboard_handlers {
#[hemx::handler] #[hemx::handler]
pub async fn open_settings(State(_ctx): State<AppContext>) -> impl IntoEffect { pub async fn open_settings(State(_ctx): State<AppContext>) -> impl IntoEffect {
( (
dashboard::page_panel.put(&SettingsPage { dashboard::page_panel.put(&SettingsPage::production_boundaries()),
message:
"Auth, CSRF, persistence, metrics, and deploy stay explicit app integrations.",
}),
dashboard::nav.set("Settings"), dashboard::nav.set("Settings"),
push("/settings"), push("/settings"),
) )
@@ -355,6 +378,31 @@ mod tests {
assert!(html.as_str().contains("/metrics.js")); assert!(html.as_str().contains("/metrics.js"));
} }
#[test]
fn settings_page_renders_the_full_page_fallback() {
// req: examples/001 req: page_swap/002
let ctx = AppContext::demo();
let html = settings_page(&ctx);
let document = ParsedHtml::parse_document(html.as_str());
assert_eq!(
document
.select(&selector("[data-hemx-root='dashboard']"))
.count(),
1
);
assert_eq!(
document
.select(&selector("[data-hemx-slot='page_panel'] .settings-page"))
.count(),
1
);
assert!(html.as_str().contains("explicit app integrations"));
assert!(!html
.as_str()
.contains("form data-hemx-handle=\"create_project\""));
}
#[tokio::test] #[tokio::test]
async fn create_project_is_auth_csrf_checked_and_persisted_locally() { async fn create_project_is_auth_csrf_checked_and_persisted_locally() {
// req: examples/001 req: auth/002 req: auth/004 req: form/001 req: failure/004 // req: examples/001 req: auth/002 req: auth/004 req: form/001 req: failure/004
+2 -2
View File
@@ -6,7 +6,7 @@ use axum::Router;
use futures_util::stream; use futures_util::stream;
use hemx::IntoEffect; use hemx::IntoEffect;
use hemx_axum::{runtime_js, sse, EffectResponse, InteractionRequest}; use hemx_axum::{runtime_js, sse, EffectResponse, InteractionRequest};
use hemx_saas_example::{home_page, live_status, registry, ui, AppContext}; use hemx_saas_example::{home_page, live_status, registry, settings_page, ui, AppContext};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::convert::Infallible; use std::convert::Infallible;
@@ -37,7 +37,7 @@ async fn home(State(ctx): State<AppContext>) -> impl IntoResponse {
} }
async fn settings(State(ctx): State<AppContext>) -> impl IntoResponse { async fn settings(State(ctx): State<AppContext>) -> impl IntoResponse {
axum::response::Html(home_page(&ctx).into_string()) axum::response::Html(settings_page(&ctx).into_string())
} }
async fn interact( async fn interact(
+5
View File
@@ -11,6 +11,7 @@
</nav> </nav>
<section class="panel" data-hemx-slot="page_panel"> <section class="panel" data-hemx-slot="page_panel">
<div h-if="self.show_projects">
<form class="project-form" data-hemx-handle="create_project" data-hemx-form="new_project" data-hemx-disable-while-pending> <form class="project-form" data-hemx-handle="create_project" data-hemx-form="new_project" data-hemx-disable-while-pending>
<input type="hidden" name="csrf" +value="self.csrf"> <input type="hidden" name="csrf" +value="self.csrf">
<label>Project name <label>Project name
@@ -28,6 +29,10 @@
{+ row +} {+ row +}
</template> </template>
</ul> </ul>
</div>
<div h-if="!self.show_projects">
{+ self.settings +}
</div>
</section> </section>
<section class="status-row"> <section class="status-row">