build: a card states what the reader must already have
`requires:` on a need is one sentence naming what the person must have or be for the alternative to work - a membership, a reference number, the link from a mail. It renders as the card's first feature, above the road, who-reads-it and the form, because a condition a reader meets only after filling the form in has already cost them the filling in. Whether the section exists is read from the plan, never from its wording: under a lift every slot holds a token and a token is never empty, which would conjure a section the live page lacks and shift every feature after it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
743f4a8563
commit
1ed33b6160
+45
-5
@@ -196,6 +196,13 @@ pub struct PlanNeed {
|
||||
/// start and not after, where they depart instead.
|
||||
#[serde(default)]
|
||||
pub turned_down_from: Vec<String>,
|
||||
/// What the person must already have or be for this alternative to
|
||||
/// work: a membership, a reference number, the link from a mail,
|
||||
/// an account. Stated on the card before the form, because a
|
||||
/// condition a reader meets only after filling something in has
|
||||
/// already cost them the filling in. Empty when there is none.
|
||||
#[serde(default)]
|
||||
pub requires: String,
|
||||
/// Who presses the button that moves a case INTO a stage, where it
|
||||
/// is not `handled_by`: another group, several groups, or
|
||||
/// `submitter` - the person who sent the form, through their own
|
||||
@@ -628,10 +635,14 @@ pub fn fallback_words(plan: &Plan) -> BTreeMap<String, Value> {
|
||||
"button": "Send",
|
||||
"encouragement": "",
|
||||
"icon": "",
|
||||
// Two sections of context the plan already knows: the road
|
||||
// a case travels, and who is at the other end. The
|
||||
// fallbacks are built from the plan, so they are true
|
||||
// before a model has said a word.
|
||||
// Three sections of context the plan already knows: what
|
||||
// the person must have for this to work, the road a case
|
||||
// travels, and who is at the other end. The fallbacks are
|
||||
// built from the plan, so they are true before a model has
|
||||
// said a word.
|
||||
"requires_title": if need.requires.trim().is_empty() { String::new() } else { "What you need".to_string() },
|
||||
"requires_description": need.requires.trim(),
|
||||
"requires_icon": if need.requires.trim().is_empty() { "" } else { "key" },
|
||||
"road_title": "What happens next",
|
||||
"road_description": stages_sentence(need),
|
||||
"road_icon": "route",
|
||||
@@ -1190,6 +1201,17 @@ fn form_alternative(plan: &Plan, need: &PlanNeed, w: &Value, action: &str) -> Va
|
||||
f
|
||||
};
|
||||
let mut features = Vec::new();
|
||||
// What the person must already have or be for this to work at all,
|
||||
// first of everything: a card that advertises something conditional
|
||||
// and says so only after the form has taken their time has already
|
||||
// wasted it.
|
||||
// Whether the section exists is the plan's to say, never the
|
||||
// wording's: under a lift every slot holds a token, and a token is
|
||||
// never empty, so a word-driven condition would conjure a section
|
||||
// the live page does not have and shift every feature after it.
|
||||
if !need.requires.trim().is_empty() {
|
||||
features.push(strip(s(&w["requires_title"]), s(&w["requires_description"]), s(&w["requires_icon"])));
|
||||
}
|
||||
if !s(&w["road_description"]).trim().is_empty() {
|
||||
features.push(strip(s(&w["road_title"]), s(&w["road_description"]), s(&w["road_icon"])));
|
||||
}
|
||||
@@ -1718,7 +1740,7 @@ mod tests {
|
||||
lands_in: "b".into(), handled_by: "g".into(),
|
||||
stages: v(stages), done_when: v(done), success: v(success), skippable: v(skippable),
|
||||
moves_by: BTreeMap::new(), watched_by: vec![],
|
||||
events: BTreeMap::new(), attended_by: None, mails: BTreeMap::new(), stages_text: BTreeMap::new(), turned_down_from: vec![],
|
||||
events: BTreeMap::new(), attended_by: None, mails: BTreeMap::new(), stages_text: BTreeMap::new(), turned_down_from: vec![], requires: String::new(),
|
||||
collects: vec![], max_fields: 3,
|
||||
}
|
||||
}
|
||||
@@ -1797,6 +1819,24 @@ mod tests {
|
||||
assert!(!m.contains_key("won"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stated_prerequisite_comes_before_the_form() {
|
||||
let yaml = "business: B\ncontact: { name: I, email: i@b.example }\nneeds:\n - id: renew\n who: members\n wants: another year\n requires: You need your membership number from last year's receipt.\n lands_in: renewals\n handled_by: office\n stages: [open, done]\n done_when: [done]\n max_fields: 2\n collects:\n - { name: number }\n";
|
||||
let plan: Plan = serde_yaml::from_str(yaml).unwrap();
|
||||
plan.validate().unwrap();
|
||||
let files = render(&plan, &fallback_words(&plan)).unwrap();
|
||||
let front: Value = serde_yaml::from_str(&files["questions/index.yaml"]).unwrap();
|
||||
let features = front["alternatives"][1]["features"].as_array().unwrap();
|
||||
assert_eq!(features[0]["name"], "What you need");
|
||||
assert_eq!(features[0]["description"], "You need your membership number from last year's receipt.");
|
||||
assert_eq!(features[1]["name"], "What happens next");
|
||||
// No prerequisite, no section.
|
||||
let none: Plan = serde_yaml::from_str(&yaml.replace(" requires: You need your membership number from last year's receipt.\n", "")).unwrap();
|
||||
let files = render(&none, &fallback_words(&none)).unwrap();
|
||||
let front: Value = serde_yaml::from_str(&files["questions/index.yaml"]).unwrap();
|
||||
assert_eq!(front["alternatives"][1]["features"][0]["name"], "What happens next");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_case_is_turned_down_only_where_the_plan_says() {
|
||||
let mut n = need(&["open", "invited", "active", "departed", "declined"], &["departed", "declined"], &["departed"], &[]);
|
||||
|
||||
Reference in New Issue
Block a user