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.
|
/// start and not after, where they depart instead.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub turned_down_from: Vec<String>,
|
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
|
/// Who presses the button that moves a case INTO a stage, where it
|
||||||
/// is not `handled_by`: another group, several groups, or
|
/// is not `handled_by`: another group, several groups, or
|
||||||
/// `submitter` - the person who sent the form, through their own
|
/// `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",
|
"button": "Send",
|
||||||
"encouragement": "",
|
"encouragement": "",
|
||||||
"icon": "",
|
"icon": "",
|
||||||
// Two sections of context the plan already knows: the road
|
// Three sections of context the plan already knows: what
|
||||||
// a case travels, and who is at the other end. The
|
// the person must have for this to work, the road a case
|
||||||
// fallbacks are built from the plan, so they are true
|
// travels, and who is at the other end. The fallbacks are
|
||||||
// before a model has said a word.
|
// 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_title": "What happens next",
|
||||||
"road_description": stages_sentence(need),
|
"road_description": stages_sentence(need),
|
||||||
"road_icon": "route",
|
"road_icon": "route",
|
||||||
@@ -1190,6 +1201,17 @@ fn form_alternative(plan: &Plan, need: &PlanNeed, w: &Value, action: &str) -> Va
|
|||||||
f
|
f
|
||||||
};
|
};
|
||||||
let mut features = Vec::new();
|
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() {
|
if !s(&w["road_description"]).trim().is_empty() {
|
||||||
features.push(strip(s(&w["road_title"]), s(&w["road_description"]), s(&w["road_icon"])));
|
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(),
|
lands_in: "b".into(), handled_by: "g".into(),
|
||||||
stages: v(stages), done_when: v(done), success: v(success), skippable: v(skippable),
|
stages: v(stages), done_when: v(done), success: v(success), skippable: v(skippable),
|
||||||
moves_by: BTreeMap::new(), watched_by: vec![],
|
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,
|
collects: vec![], max_fields: 3,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1797,6 +1819,24 @@ mod tests {
|
|||||||
assert!(!m.contains_key("won"));
|
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]
|
#[test]
|
||||||
fn a_case_is_turned_down_only_where_the_plan_says() {
|
fn a_case_is_turned_down_only_where_the_plan_says() {
|
||||||
let mut n = need(&["open", "invited", "active", "departed", "declined"], &["departed", "declined"], &["departed"], &[]);
|
let mut n = need(&["open", "invited", "active", "departed", "declined"], &["departed", "declined"], &["departed"], &[]);
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ needs:
|
|||||||
done_when: [<end>, <end>] # every stage that counts as finished, good or bad
|
done_when: [<end>, <end>] # every stage that counts as finished, good or bad
|
||||||
success: [<end>] # the finished stages that are a good outcome for the business
|
success: [<end>] # the finished stages that are a good outcome for the business
|
||||||
skippable: [<stage>] # stages a case may pass over (papers already complete)
|
skippable: [<stage>] # stages a case may pass over (papers already complete)
|
||||||
|
requires: <one sentence> # what the person must ALREADY have or be for this to work:
|
||||||
|
# a membership, a reference number, the link from a mail, an
|
||||||
|
# account. Stated on the card above the form. Omit when there
|
||||||
|
# is none; never invent one
|
||||||
turned_down_from: [open, <stage>]
|
turned_down_from: [open, <stage>]
|
||||||
# the unfinished stages a case can still be turned down from;
|
# the unfinished stages a case can still be turned down from;
|
||||||
# omit for every one. An applicant is declined before they
|
# omit for every one. An applicant is declined before they
|
||||||
@@ -122,6 +126,11 @@ Rules:
|
|||||||
- Every need whose form collects an email should tell the person on
|
- Every need whose form collects an email should tell the person on
|
||||||
arrival and on the decisions about them (`mails`); the plan then
|
arrival and on the decisions about them (`mails`); the plan then
|
||||||
needs `mail: { from }`.
|
needs `mail: { from }`.
|
||||||
|
- Every feature on a card explains itself: a list says what one row is
|
||||||
|
and what its buttons do, a form says what to have ready. If an
|
||||||
|
alternative only works for someone who already has something, say so
|
||||||
|
in `requires` - a condition the reader meets only after filling the
|
||||||
|
form in has already cost them the filling in.
|
||||||
- Needs that land in the same bucket must have the same stages,
|
- Needs that land in the same bucket must have the same stages,
|
||||||
done_when, skippable, moves_by, watched_by and handled_by.
|
done_when, skippable, moves_by, watched_by and handled_by.
|
||||||
- Every required field is one the business will use to act on the
|
- Every required field is one the business will use to act on the
|
||||||
|
|||||||
Reference in New Issue
Block a user