- QuestionNav: every other question the visitor currently qualifies for, rendered under each page's alternatives - a concern reaches the people it speaks to without claiming front-page space, and an owner sees the gated desks in the same nav. Context-dependent by session (list_qualifying_questions filters via is_qualified). - An alternative with an action but nothing to submit, confirm, or record is a gateway: its button now renders as a real link instead of a form submit that did nothing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
4d71b6ccd0
commit
e237a4b3c0
+107
-22
@@ -196,19 +196,23 @@ fn QuestionView(
|
||||
}
|
||||
}
|
||||
key=|a| a.name.clone()
|
||||
children=move |alt: Alternative| {
|
||||
view! {
|
||||
<AlternativeCard
|
||||
question_id=question_id.clone()
|
||||
alternative=alt
|
||||
parent_hash=parent_hash.clone()
|
||||
query_email=query_email.clone()
|
||||
/>
|
||||
children={
|
||||
let question_id = question_id.clone();
|
||||
move |alt: Alternative| {
|
||||
view! {
|
||||
<AlternativeCard
|
||||
question_id=question_id.clone()
|
||||
alternative=alt
|
||||
parent_hash=parent_hash.clone()
|
||||
query_email=query_email.clone()
|
||||
/>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<QuestionNav current_id=question_id/>
|
||||
{question
|
||||
.responsible
|
||||
.clone()
|
||||
@@ -219,6 +223,39 @@ fn QuestionView(
|
||||
.into_any()
|
||||
}
|
||||
|
||||
/// Every other question the visitor currently qualifies for, as a
|
||||
/// small nav - how a concern reaches the people it speaks to without
|
||||
/// claiming front-page space (an owner also sees the gated desks here).
|
||||
#[component]
|
||||
fn QuestionNav(current_id: String) -> impl IntoView {
|
||||
let nav = Resource::new(|| (), |_| list_qualifying_questions());
|
||||
view! {
|
||||
<Suspense fallback=|| ()>
|
||||
{move || {
|
||||
let current_id = current_id.clone();
|
||||
nav.get().and_then(|res| res.ok()).map(|items| {
|
||||
let others: Vec<(String, String)> = items
|
||||
.into_iter()
|
||||
.filter(|(id, _)| *id != current_id)
|
||||
.collect();
|
||||
(!others.is_empty()).then(|| view! {
|
||||
<nav class="question-nav">
|
||||
<span class="question-nav-lead">"Also worth asking"</span>
|
||||
<For
|
||||
each=move || others.clone()
|
||||
key=|(id, _)| id.clone()
|
||||
children=|(id, name): (String, String)| {
|
||||
view! { <a href=id>{name}</a> }.into_any()
|
||||
}
|
||||
/>
|
||||
</nav>
|
||||
})
|
||||
})
|
||||
}}
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
/// "Asked by X — contact them if you get stuck."
|
||||
/// The mailto address is assembled from `data-user`/`data-domain` on a
|
||||
/// real mouse event, never baked into the server-rendered `href` -
|
||||
@@ -463,6 +500,16 @@ fn AlternativeCard(
|
||||
|
||||
let has_action = alternative.action.is_some();
|
||||
let has_requirements = alternative.features.iter().any(|f| !f.requirements.is_empty());
|
||||
// Nothing to submit, nothing to confirm, nothing to record: the
|
||||
// alternative is a gateway to another question, and its button is
|
||||
// just a link (a form submit would be a click that does nothing).
|
||||
let is_gateway = has_action
|
||||
&& !has_requirements
|
||||
&& alternative.record_as.is_none()
|
||||
&& alternative
|
||||
.features
|
||||
.iter()
|
||||
.all(|f| f.resource.as_ref().is_none_or(|r| r.transitions.is_empty()));
|
||||
|
||||
// Shared across every resource feature on this alternative: which
|
||||
// row(s) have a transition selected but not yet confirmed, keyed by
|
||||
@@ -841,19 +888,30 @@ fn AlternativeCard(
|
||||
children=move |e| view! { <p class="alt-encouragement">{e}</p> }
|
||||
/>
|
||||
</div>
|
||||
<form on:submit=on_submit>
|
||||
<button
|
||||
type="submit"
|
||||
class="alt-submit"
|
||||
disabled=move || {
|
||||
(!has_requirements && pending_transitions.get().is_empty())
|
||||
|| submit.pending().get()
|
||||
|| transition_batch.pending().get()
|
||||
}
|
||||
>
|
||||
{button_label.clone()}
|
||||
</button>
|
||||
</form>
|
||||
{if is_gateway {
|
||||
let href = alternative.action.clone().unwrap_or_default();
|
||||
view! {
|
||||
<a class="alt-submit" href=href>{button_label.clone()}</a>
|
||||
}
|
||||
.into_any()
|
||||
} else {
|
||||
view! {
|
||||
<form on:submit=on_submit>
|
||||
<button
|
||||
type="submit"
|
||||
class="alt-submit"
|
||||
disabled=move || {
|
||||
(!has_requirements && pending_transitions.get().is_empty())
|
||||
|| submit.pending().get()
|
||||
|| transition_batch.pending().get()
|
||||
}
|
||||
>
|
||||
{button_label.clone()}
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
.into_any()
|
||||
}}
|
||||
</div>
|
||||
}
|
||||
})}
|
||||
@@ -1372,6 +1430,33 @@ pub async fn get_question(path: String) -> Result<Option<Question>, ServerFnErro
|
||||
Ok(state.questions.load().get(&path).cloned())
|
||||
}
|
||||
|
||||
/// Every question the current visitor qualifies for, as (id, name) -
|
||||
/// the site nav's data. Context-dependent: an owner session sees the
|
||||
/// gated pages too, an anonymous visitor only the public ones.
|
||||
#[server]
|
||||
pub async fn list_qualifying_questions() -> Result<Vec<(String, String)>, ServerFnError> {
|
||||
use crate::auth::{User, SESSION_USER_KEY};
|
||||
use crate::content::is_qualified;
|
||||
use crate::server::AppState;
|
||||
|
||||
let state = expect_context::<AppState>();
|
||||
let session: tower_sessions::Session = leptos_axum::extract().await?;
|
||||
let user = session
|
||||
.get::<User>(SESSION_USER_KEY)
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
|
||||
let mut out: Vec<(String, String)> = state
|
||||
.questions
|
||||
.load()
|
||||
.values()
|
||||
.filter(|q| is_qualified(user.as_ref(), q))
|
||||
.map(|q| (q.id.clone(), q.name.clone()))
|
||||
.collect();
|
||||
out.sort();
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SubmitResult {
|
||||
pub next: Option<String>,
|
||||
|
||||
Reference in New Issue
Block a user