From 6df895b1d3a61b935b227f80e2947a455bd69918 Mon Sep 17 00:00:00 2001 From: Bendik Aagaard Lynghaug Date: Wed, 5 Aug 2026 15:04:02 +0200 Subject: [PATCH] Fix get_resource/transition_answer: scope feature lookup to its own alternative Both resolved a feature by name flattened across every alternative on the question, not scoped to the one the caller actually meant - fine when every question had exactly one alternative with a resource feature, silently wrong now that /review has three (Applicants, Inquiries, Subscribers all use the same empty feature name). Every lookup always resolved to the first alternative's feature - Subscribers was reading the (empty) Applicants bucket instead of its own, and Invite/Decline/Mark-handled buttons would have had the same problem had two of those transitions ever been clicked side by side. Threaded a new `alternative` parameter through get_resource, transition_answer, and their client-side callers (ResourceFeature/ResourceValue/AnswerRow) - the fix lives entirely in the already-generic resource-fetching machinery, no per-alternative special-casing. --- src/answers.rs | 9 ++++++++- src/app.rs | 18 ++++++++++++++++-- src/resource.rs | 21 +++++++++++++++++---- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/answers.rs b/src/answers.rs index 830d451..07044aa 100644 --- a/src/answers.rs +++ b/src/answers.rs @@ -84,6 +84,7 @@ pub async fn store_answer( #[server] pub async fn transition_answer( question_id: String, + alternative: String, feature_name: String, item_id: String, to: String, @@ -100,10 +101,16 @@ pub async fn transition_answer( .get(&question_id) .cloned() .ok_or_else(|| ServerFnError::new("unknown question"))?; + // Same alternative-scoped lookup as get_resource, and for the same + // reason - a feature name isn't unique across a whole question, + // only within its own alternative. let feature = question .alternatives .iter() - .flat_map(|a| &a.features) + .find(|a| a.name == alternative) + .ok_or_else(|| ServerFnError::new("unknown alternative"))? + .features + .iter() .find(|f| f.name == feature_name) .ok_or_else(|| ServerFnError::new("unknown feature"))?; let resource = feature diff --git a/src/app.rs b/src/app.rs index c0f27e8..c9dea13 100644 --- a/src/app.rs +++ b/src/app.rs @@ -420,11 +420,13 @@ fn AlternativeCard( let file_refs = file_refs.clone(); let field_prefix = field_prefix.clone(); let question_id = question_id.clone(); + let alt_name = alternative.name.clone(); move |feature| { let field_map = field_map.clone(); let file_refs = file_refs.clone(); let field_prefix = field_prefix.clone(); let question_id = question_id.clone(); + let alt_name = alt_name.clone(); let resource = feature.resource.clone(); let feature_name = feature.name.clone(); view! { @@ -445,6 +447,7 @@ fn AlternativeCard( view! { @@ -603,16 +606,18 @@ fn AlternativeCard( #[component] fn ResourceFeature( question_id: String, + alternative: String, feature_name: String, transitions: Vec, ) -> impl IntoView { let data = Resource::new( { let question_id = question_id.clone(); + let alternative = alternative.clone(); let feature_name = feature_name.clone(); - move || (question_id.clone(), feature_name.clone()) + move || (question_id.clone(), alternative.clone(), feature_name.clone()) }, - |(q, f)| get_resource(q, f), + |(q, a, f)| get_resource(q, a, f), ); view! { @@ -620,6 +625,7 @@ fn ResourceFeature( "loading…"

}> {move || { let question_id = question_id.clone(); + let alternative = alternative.clone(); let feature_name = feature_name.clone(); let transitions = transitions.clone(); data.get() @@ -628,6 +634,7 @@ fn ResourceFeature( view! { , value: serde_json::Value, @@ -671,12 +679,14 @@ fn ResourceValue( key=|a| a.id.clone() children={ let question_id = question_id.clone(); + let alternative = alternative.clone(); let feature_name = feature_name.clone(); let transitions = transitions.clone(); move |answer: Answer| { view! { , answer: Answer, @@ -755,10 +766,12 @@ fn AnswerRow( key=|t| t.to.clone() children={ let question_id = question_id.clone(); + let alternative = alternative.clone(); let feature_name = feature_name.clone(); let item_id = item_id.clone(); move |t: Transition| { let question_id = question_id.clone(); + let alternative = alternative.clone(); let feature_name = feature_name.clone(); let item_id = item_id.clone(); let to = t.to.clone(); @@ -768,6 +781,7 @@ fn AnswerRow( on:click=move |_| { transition.dispatch(TransitionAnswer { question_id: question_id.clone(), + alternative: alternative.clone(), feature_name: feature_name.clone(), item_id: item_id.clone(), to: to.clone(), diff --git a/src/resource.rs b/src/resource.rs index cd7e234..71df30d 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -8,12 +8,14 @@ use leptos::prelude::*; -/// Fetches the live data for `question_id`'s `feature_name` feature. -/// Fails closed: a resource with neither `public: true` nor -/// `requires_group` set is unreachable, not "open" by omission. +/// Fetches the live data for `question_id`'s `alternative`'s +/// `feature_name` feature. Fails closed: a resource with neither +/// `public: true` nor `requires_group` set is unreachable, not "open" +/// by omission. #[server] pub async fn get_resource( question_id: String, + alternative: String, feature_name: String, ) -> Result { use crate::auth::{User, SESSION_USER_KEY}; @@ -26,10 +28,21 @@ pub async fn get_resource( .get(&question_id) .cloned() .ok_or_else(|| ServerFnError::new("unknown question"))?; + // Scoped to the named alternative first, not flattened across all + // of them - a feature name (often just "") is only unique within + // its own alternative, not across a whole question. Flattening + // silently resolved every same-named feature to whichever + // alternative happened to be first, so "Subscribers" (and any + // other later resource-listing alternative sharing an unnamed + // feature with an earlier one on the same question) always read + // the first alternative's bucket instead of its own. let feature = question .alternatives .iter() - .flat_map(|a| &a.features) + .find(|a| a.name == alternative) + .ok_or_else(|| ServerFnError::new("unknown alternative"))? + .features + .iter() .find(|f| f.name == feature_name) .ok_or_else(|| ServerFnError::new("unknown feature"))?; let resource = feature