Fix get_resource/transition_answer: scope feature lookup to its own alternative
Deploy / deploy (push) Successful in 28s
Deploy / deploy (push) Successful in 28s
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.
This commit is contained in:
+8
-1
@@ -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
|
||||
|
||||
+16
-2
@@ -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! {
|
||||
<ResourceFeature
|
||||
question_id=question_id.clone()
|
||||
alternative=alt_name.clone()
|
||||
feature_name=feature_name.clone()
|
||||
transitions=spec.transitions.clone()
|
||||
/>
|
||||
@@ -603,16 +606,18 @@ fn AlternativeCard(
|
||||
#[component]
|
||||
fn ResourceFeature(
|
||||
question_id: String,
|
||||
alternative: String,
|
||||
feature_name: String,
|
||||
transitions: Vec<Transition>,
|
||||
) -> 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(
|
||||
<Suspense fallback=|| view! { <p class="resource-loading">"loading…"</p> }>
|
||||
{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! {
|
||||
<ResourceValue
|
||||
question_id=question_id
|
||||
alternative=alternative
|
||||
feature_name=feature_name
|
||||
transitions=transitions
|
||||
value=value
|
||||
@@ -653,6 +660,7 @@ fn ResourceFeature(
|
||||
#[component]
|
||||
fn ResourceValue(
|
||||
question_id: String,
|
||||
alternative: String,
|
||||
feature_name: String,
|
||||
transitions: Vec<Transition>,
|
||||
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! {
|
||||
<AnswerRow
|
||||
question_id=question_id.clone()
|
||||
alternative=alternative.clone()
|
||||
feature_name=feature_name.clone()
|
||||
transitions=transitions.clone()
|
||||
answer=answer
|
||||
@@ -701,6 +711,7 @@ fn ResourceValue(
|
||||
#[component]
|
||||
fn AnswerRow(
|
||||
question_id: String,
|
||||
alternative: String,
|
||||
feature_name: String,
|
||||
transitions: Vec<Transition>,
|
||||
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(),
|
||||
|
||||
+17
-4
@@ -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<serde_json::Value, ServerFnError> {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user