Attended-bucket lint rule; app-lifetime resources fix first-nav corruption
Test / test (push) Successful in 25s
Test / test (push) Successful in 25s
validate_questions now rejects content where a record_as bucket is read by nothing: every bucket must be listed by some Kv resource in the same repo (a desk) or carry aggregates.yaml's new attended_by annotation naming the automation that consumes it - no publicly collected answer may land where nothing reads. Separately, all server-fn resources (question/user/nav) move to a PortalShell above the routes, created once and provided via context. Per-page Resources broke on the first client-side navigation: the remounted component's fresh Resource consumed a stale SSR hydration buffer - the nav list [[id, name], ..] deserialized as a Page (serde fills structs from sequences in field order), so uhhm.no's landing question rendered chain-gated behind its own nav entry instead of the /develop/proposal form. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
0d6cdb8a00
commit
a1fc333079
+50
-11
@@ -81,16 +81,34 @@ pub fn App() -> impl IntoView {
|
||||
}}
|
||||
</Suspense>
|
||||
<Router>
|
||||
<Routes fallback=|| view! { <NotFound/> }>
|
||||
<Route path=path!("") view=QuestionPage/>
|
||||
<Route path=path!("/*any") view=QuestionPage/>
|
||||
</Routes>
|
||||
<PortalShell/>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
/// Every server-fn resource the pages read, created exactly once for
|
||||
/// the app's lifetime and handed down via context. Wrapper structs
|
||||
/// because a bare `Resource<T>` context is claimed by whoever provides
|
||||
/// that T last.
|
||||
#[derive(Clone, Copy)]
|
||||
struct QuestionRes(Resource<Result<Option<Page>, ServerFnError>>);
|
||||
#[derive(Clone, Copy)]
|
||||
struct UserRes(Resource<Result<Option<User>, ServerFnError>>);
|
||||
#[derive(Clone, Copy)]
|
||||
struct NavRes(Resource<Result<Vec<(String, String)>, ServerFnError>>);
|
||||
|
||||
/// Owns the app's data resources, above the routes and reactive on the
|
||||
/// location instead of recreated per page. Route components creating
|
||||
/// their own resources broke on the first client-side navigation: the
|
||||
/// remounted component's fresh Resource consumed a stale SSR hydration
|
||||
/// buffer instead of fetching - concretely, the nav list
|
||||
/// `[[id, name], ..]` deserialized as a `Page` (serde fills a struct
|
||||
/// from a sequence in field order), rendering the landing question
|
||||
/// gated behind its own nav entry. Stable resources + context makes
|
||||
/// that class of misalignment impossible: navigation only changes a
|
||||
/// key, and a key change always refetches.
|
||||
#[component]
|
||||
fn QuestionPage() -> impl IntoView {
|
||||
fn PortalShell() -> impl IntoView {
|
||||
let location = use_location();
|
||||
let query = use_query_map();
|
||||
let path = Memo::new(move |_| {
|
||||
@@ -101,16 +119,36 @@ fn QuestionPage() -> impl IntoView {
|
||||
p
|
||||
}
|
||||
});
|
||||
let parent_hash = Memo::new(move |_| query.with(|q| q.get("chain")));
|
||||
let query_email = Memo::new(move |_| query.with(|q| q.get("email")));
|
||||
let chain = Memo::new(move |_| query.with(|q| q.get("chain")));
|
||||
|
||||
let question = Resource::new(
|
||||
move || (path.get(), parent_hash.get()),
|
||||
move || (path.get(), chain.get()),
|
||||
|(path, chain)| get_question(path, chain),
|
||||
);
|
||||
let user = Resource::new(|| (), |_| current_user());
|
||||
// App's shared site resource (see App) - one fetch per session,
|
||||
// and the Title tied to it survives navigation.
|
||||
let nav = Resource::new(move || chain.get().is_some(), list_qualifying_questions);
|
||||
provide_context(QuestionRes(question));
|
||||
provide_context(UserRes(user));
|
||||
provide_context(NavRes(nav));
|
||||
|
||||
view! {
|
||||
<Routes fallback=|| view! { <NotFound/> }>
|
||||
<Route path=path!("") view=QuestionPage/>
|
||||
<Route path=path!("/*any") view=QuestionPage/>
|
||||
</Routes>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn QuestionPage() -> impl IntoView {
|
||||
let query = use_query_map();
|
||||
let parent_hash = Memo::new(move |_| query.with(|q| q.get("chain")));
|
||||
let query_email = Memo::new(move |_| query.with(|q| q.get("email")));
|
||||
|
||||
// All shared, app-lifetime resources (see PortalShell / App) -
|
||||
// never created per navigation.
|
||||
let QuestionRes(question) = expect_context();
|
||||
let UserRes(user) = expect_context();
|
||||
let site = expect_context::<Resource<Result<SiteConfig, ServerFnError>>>();
|
||||
|
||||
view! {
|
||||
@@ -295,7 +333,8 @@ fn QuestionView(
|
||||
/// claiming front-page space (an owner also sees the gated desks here).
|
||||
#[component]
|
||||
fn QuestionNav(current_id: String, has_chain: bool) -> impl IntoView {
|
||||
let nav = Resource::new(move || has_chain, list_qualifying_questions);
|
||||
let _ = has_chain; // keyed into the shared resource by PortalShell
|
||||
let NavRes(nav) = expect_context();
|
||||
view! {
|
||||
<Suspense fallback=|| ()>
|
||||
{move || {
|
||||
|
||||
Reference in New Issue
Block a user