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