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,
rows: Vec<ProjectRow>,
project_count: usize,
show_projects: bool,
settings: SettingsPage,
}
impl Dashboard {
@@ -191,9 +193,18 @@ impl Dashboard {
flash: "Signed in with a demo session".to_owned(),
summary: project_summary(projects.len()),
project_count: projects.len(),
show_projects: true,
settings: SettingsPage::production_boundaries(),
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)]
@@ -226,6 +237,14 @@ pub struct SettingsPage {
message: &'static str,
}
impl SettingsPage {
fn production_boundaries() -> Self {
Self {
message: "Auth, CSRF, persistence, metrics, and deploy stay explicit app integrations.",
}
}
}
#[derive(Hemplate)]
pub struct AppShell {
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)]
pub fn registry(ctx: AppContext) -> Registry {
interactions(ui::BUILD_FINGERPRINT)
@@ -277,10 +303,7 @@ mod dashboard_handlers {
#[hemx::handler]
pub async fn open_settings(State(_ctx): State<AppContext>) -> impl IntoEffect {
(
dashboard::page_panel.put(&SettingsPage {
message:
"Auth, CSRF, persistence, metrics, and deploy stay explicit app integrations.",
}),
dashboard::page_panel.put(&SettingsPage::production_boundaries()),
dashboard::nav.set("Settings"),
push("/settings"),
)
@@ -355,6 +378,31 @@ mod tests {
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]
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
+2 -2
View File
@@ -6,7 +6,7 @@ use axum::Router;
use futures_util::stream;
use hemx::IntoEffect;
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::convert::Infallible;
@@ -37,7 +37,7 @@ async fn home(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(
+20 -15
View File
@@ -11,23 +11,28 @@
</nav>
<section class="panel" data-hemx-slot="page_panel">
<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">
<label>Project name
<input name="name" required="required" maxlength="64" value="Launch checklist">
</label>
<button type="submit">Create project</button>
<p class="field-error" data-hemx-error-for="name"></p>
</form>
<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>
<input type="hidden" name="csrf" +value="self.csrf">
<label>Project name
<input name="name" required="required" maxlength="64" value="Launch checklist">
</label>
<button type="submit">Create project</button>
<p class="field-error" data-hemx-error-for="name"></p>
</form>
<p class="flash" data-hemx-slot="flash">{+ self.flash +}</p>
<p class="summary" data-hemx-slot="summary">{+ self.summary +}</p>
<p class="flash" data-hemx-slot="flash">{+ self.flash +}</p>
<p class="summary" data-hemx-slot="summary">{+ self.summary +}</p>
<ul class="project-list" data-hemx-slot="project_row">
<template h-for="row in &self.rows" h-key="row.id">
{+ row +}
</template>
</ul>
<ul class="project-list" data-hemx-slot="project_row">
<template h-for="row in &self.rows" h-key="row.id">
{+ row +}
</template>
</ul>
</div>
<div h-if="!self.show_projects">
{+ self.settings +}
</div>
</section>
<section class="status-row">