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:
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -11,23 +11,28 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<section class="panel" data-hemx-slot="page_panel">
|
<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>
|
<div h-if="self.show_projects">
|
||||||
<input type="hidden" name="csrf" +value="self.csrf">
|
<form class="project-form" data-hemx-handle="create_project" data-hemx-form="new_project" data-hemx-disable-while-pending>
|
||||||
<label>Project name
|
<input type="hidden" name="csrf" +value="self.csrf">
|
||||||
<input name="name" required="required" maxlength="64" value="Launch checklist">
|
<label>Project name
|
||||||
</label>
|
<input name="name" required="required" maxlength="64" value="Launch checklist">
|
||||||
<button type="submit">Create project</button>
|
</label>
|
||||||
<p class="field-error" data-hemx-error-for="name"></p>
|
<button type="submit">Create project</button>
|
||||||
</form>
|
<p class="field-error" data-hemx-error-for="name"></p>
|
||||||
|
</form>
|
||||||
|
|
||||||
<p class="flash" data-hemx-slot="flash">{+ self.flash +}</p>
|
<p class="flash" data-hemx-slot="flash">{+ self.flash +}</p>
|
||||||
<p class="summary" data-hemx-slot="summary">{+ self.summary +}</p>
|
<p class="summary" data-hemx-slot="summary">{+ self.summary +}</p>
|
||||||
|
|
||||||
<ul class="project-list" data-hemx-slot="project_row">
|
<ul class="project-list" data-hemx-slot="project_row">
|
||||||
<template h-for="row in &self.rows" h-key="row.id">
|
<template h-for="row in &self.rows" h-key="row.id">
|
||||||
{+ 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">
|
||||||
|
|||||||
Reference in New Issue
Block a user