Bound inputs: a field that loads its value from a sibling-parameterized resource
Deploy / deploy (push) Successful in 1m7s
Deploy / deploy (push) Successful in 1m7s
Requirement.bind {field, param, resource}: when the watched sibling
changes (a file select, say), the bind's resource is fetched with the
sibling's value as a parameter and the result becomes this field's
value - the on-site editing flow for /develop-proposal, and a generic
select-plus-detail primitive anywhere else. Url sources gain {name}
path templating (percent-encoded, / preserved, SSRF check runs after
substitution); an empty sibling never fetches and never clears an
edit. Bound textareas render monospace.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3d8af23167
commit
357d755b8c
+54
-1
@@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::answers::{Answer, SelfTransitionAnswer, TransitionAnswers, TransitionItem};
|
||||
use crate::auth::{current_user, User};
|
||||
use crate::content::{is_qualified, Alternative, Question, Responsible, Transition};
|
||||
use crate::resource::{get_requirement_options, get_resource};
|
||||
use crate::resource::{get_requirement_binding, get_requirement_options, get_resource};
|
||||
|
||||
/// The visible site name/wordmark - "portal" is just this codebase's
|
||||
/// working title, not necessarily what any given deployment is called.
|
||||
@@ -758,6 +758,58 @@ fn AlternativeCard(
|
||||
|
||||
let sig = field_map.get(&req.name).copied().unwrap_or_else(|| RwSignal::new(String::new()));
|
||||
|
||||
// A bound field watches a sibling and loads its own value from
|
||||
// the bind's resource whenever the sibling changes. An empty
|
||||
// sibling never fetches - and never clears an edit.
|
||||
if let Some(bind) = req
|
||||
.bind
|
||||
.clone()
|
||||
.filter(|_| !matches!(req.kind.as_str(), "select" | "file"))
|
||||
{
|
||||
let watched_select = select_field_map.get(&bind.field).copied();
|
||||
let watched_text = field_map.get(&bind.field).copied();
|
||||
let param_name = bind.param_name().to_string();
|
||||
let q = question_id.clone();
|
||||
let a = alt_name.clone();
|
||||
let f = feature_name.clone();
|
||||
let rname = req.name.clone();
|
||||
let bound = Resource::new(
|
||||
move || {
|
||||
let value = watched_select
|
||||
.map(|s| s.get().first().cloned().unwrap_or_default())
|
||||
.or_else(|| watched_text.map(|s| s.get()))
|
||||
.unwrap_or_default();
|
||||
(q.clone(), a.clone(), f.clone(), rname.clone(), param_name.clone(), value)
|
||||
},
|
||||
|(q, a, f, r, p, value)| async move {
|
||||
if value.is_empty() {
|
||||
return Ok(serde_json::Value::Null);
|
||||
}
|
||||
let params = std::collections::HashMap::from([(p, value)]);
|
||||
get_requirement_binding(q, a, f, r, params).await
|
||||
},
|
||||
);
|
||||
Effect::new(move |_| {
|
||||
if let Some(Ok(value)) = bound.get() {
|
||||
let text = match value {
|
||||
serde_json::Value::Null => return,
|
||||
serde_json::Value::String(s) => s,
|
||||
// apply_jq wraps every yield in an array - a
|
||||
// single string yield unwraps to itself.
|
||||
serde_json::Value::Array(items) if items.len() == 1 => {
|
||||
match &items[0] {
|
||||
serde_json::Value::String(s) => s.clone(),
|
||||
other => serde_json::to_string_pretty(other).unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
other => serde_json::to_string_pretty(&other).unwrap_or_default(),
|
||||
};
|
||||
sig.set(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
let is_bound = req.bind.is_some();
|
||||
|
||||
if req.kind == "select" {
|
||||
let select_sig = select_field_map
|
||||
.get(&req.name)
|
||||
@@ -864,6 +916,7 @@ fn AlternativeCard(
|
||||
view! {
|
||||
<textarea
|
||||
id=field_id.clone()
|
||||
class:bound=is_bound
|
||||
placeholder=placeholder.clone()
|
||||
required=required
|
||||
prop:value=move || sig.get()
|
||||
|
||||
Reference in New Issue
Block a user