Resource-backed multi/single-select requirement
Deploy / deploy (push) Successful in 34s

Requirement gains an optional `resource` (reuses ResourceSpec/
ResourceSource/jq wholesale - a resource is a resource whether it's
displayed read-only or offered as choices to pick from) and `id_field`
(which field in each item is its stable id, defaults to _id then id).
`type: select` + `multiple` (already-existing field, previously file
-only) picks single vs multi. New get_requirement_options server fn
shares its auth/fetch/jq logic with get_resource via two extracted
helpers rather than duplicating it.

Submitted value is the selected id (single) or a JSON array of ids
(multi) - a new select_field_map (RwSignal<Vec<String>>, alongside the
existing field_map/file_refs maps, since a multi-select's value is a
set, not a string) threaded through the same nested <For> structure
the other requirement kinds already use.

Content validation extended: a `type: select` requirement declaring no
resource now fails at load time instead of rendering a dead field.
Caught a real bug in my own first version of that check while testing
it - it was nested inside a feature-level resource guard, so it never
ran unless the *feature* also happened to have its own resource.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Bendik Aagaard Lynghaug
2026-08-06 11:57:15 +02:00
co-authored by Claude Sonnet 5
parent 43fa496778
commit 755b796ad1
4 changed files with 324 additions and 43 deletions
+25 -1
View File
@@ -209,13 +209,29 @@ pub struct Requirement {
pub kind: String,
#[serde(default)]
pub optional: bool,
/// `type: file` only - accept multiple files.
/// `type: file` - accept multiple files. `type: select` - pick more
/// than one option (checkbox-style toggle) instead of exactly one
/// (radio-style); the submitted value is a JSON array of ids
/// instead of a single id string.
#[serde(default)]
pub multiple: bool,
/// `type: file` only - HTML `accept` hint (UX only, not a security
/// boundary - the upload handler re-checks content-type itself).
#[serde(default)]
pub accept: Option<String>,
/// `type: select` only - where the selectable options come from.
/// Reuses the exact same `ResourceSpec`/`ResourceSource`/`jq`
/// mechanism a `Feature.resource` uses (`resource::get_requirement_options`)
/// - a resource is a resource regardless of whether it's displayed
/// read-only or offered as choices to pick from.
#[serde(default)]
pub resource: Option<ResourceSpec>,
/// `type: select` only - which field in each resource item is that
/// option's stable identifier, submitted as the requirement's value
/// (or one entry of it, if `multiple`). Defaults to trying `_id`
/// then `id` if unset.
#[serde(default)]
pub id_field: Option<String>,
}
fn default_requirement_type() -> String {
@@ -372,6 +388,14 @@ pub fn validate_questions(
}
}
for feature in &alternative.features {
for requirement in &feature.requirements {
if requirement.kind == "select" && requirement.resource.is_none() {
anyhow::bail!(
"question {:?} alternative {:?} feature {:?}: requirement {:?} is type: select but declares no resource to select from",
question.id, alternative.name, feature.name, requirement.name
);
}
}
let Some(resource) = &feature.resource else {
continue;
};