avniproject / avniproject/rules-config
Explicit "show no options" for coded/group-member select fields
- Dominant language
- JavaScript
- Stars
- 0
- Forks
- 3
- PR merge metrics
- No merged PRs in 30d
Description
> ### Status: deferred — not scheduled
>
> Rules-authoring enabler. No funded consumer and no multi-org demand as of 21 Jul 2026.
>
> **Evidence:** 0 of 22 *bundled* org rule repos call `showAnswers` (its sibling `skipAnswers` appears in 12). **That sample is incomplete and known to be so** — it misses rules authored inline via the webapp editors, which is now the dominant path. avni-client#2012 (21 Jul 2026) shows `showAnswers` in live use against Subject concept types, and its sample rule names the empty-means-all behaviour directly (*"empty list shows all members"*). Demand here is therefore **unmeasured, not zero**.
>
> **Pick up if** either — (a) demand goes education-sector-wide: a second org beyond Gubbachi asks; or (b) Gubbachi or another org funds it.
>
> Assessed against `avni-product-ops/context/platform-feature-criteria.md` § 5 (bar: ≥3 distinct orgs carrying a hand-written copy). Caveat: the sample is the 22 bundled org rule repos, several of which are stale; rules authored inline via the webapp editors are not covered.
>
> The body below is **dev-ready** if this is picked up.
---
## Motivation
As a rule author, I need an explicit way to render a coded or group-member select field with
**zero** options, instead of relying on an undiscoverable sentinel-UUID hack that happens to work
by accident. Today, returning an empty answer list from `showAnswers()` doesn't mean "show
nothing" — for these two field types it means the opposite: "show everything."
---
## Context — the empty-means-all footgun
For **coded** and **group-member (subject-multiselect)** fields, an empty `answersToShow` is
interpreted as "no restriction," which these fields render as "show all," not "show none":
- **Coded fields** — `FormElement.getAnswers()` (avni-models, `src/application/FormElement.js:283-297`):
```js
getAnswers() {
const allAnswers = this.concept.getAnswers().filter((ca) => !ca.concept.voided);
if (!_.isEmpty(this.answersToShow)) {
return _.filter(allAnswers, (allConceptAnswer) => _.includes(this.answersToShow, allConceptAnswer.concept.name));
} else {
// empty answersToShow => no filter applied => full concept answer set (minus explicit excludes)
...
}
}
```
Non-empty `answersToShow` filters to just those; **empty returns the full defined answer set**.
Confirmed consumed directly by the coded-select render path:
`avni-webapp/src/dataEntryApp/components/CodedConceptFormElement.jsx:13` (`fe.getAnswers()`).
- **Group-member fields** — `FormElement.getApplicableSubjectUUIDs()` (avni-models,
`src/application/FormElement.js:303-306`):
```js
getApplicableSubjectUUIDs() {
if (!_.isEmpty(this.answersToShow)) return [...this.answersToShow];
return null; // null = "no restriction" to every downstream consumer
}
```
This `null` is treated as "don't filter" by the validation-layer consumer
`avni-models/src/ObservationsHolder.js:170-177` (`removeNonApplicableSubjectAnswers`): when
`allowedUUIDs` is `null`, the filter predicate never rejects a value, so nothing gets pruned —
i.e. unrestricted.
Both `answersToShow` fields are populated at runtime by the rules-config builder output
(`FormElementStatus.answersToShow`, `rules-config/src/rules/model/FormElementStatus.js`) and
applied onto the client-side `FormElement` instance via its transient setter
(avni-models `FormElement.js:18` declares it transient, `:191-192` is the `set setAnswersToShow`).
**The empty-means-all behaviour lives in avni-models, not rules-config** — rules-config only
produces the (possibly empty) list; avni-models/webapp decide what an empty list means.
Concrete example (from #38, retained): "Show no students until morning attendance is taken" — a
rule author wants a group-member select to render zero selectable members. Returning `[]` from
`showAnswers()` instead reveals the entire class roster.
**Scope note:** this footgun is specific to coded and group-member fields. A plain subject-search
field behaves differently: `avni-webapp/src/dataEntryApp/components/SubjectFormElement.jsx:22-24`
computes `hasAllowedList = !isEmpty(allowedSubjectUUIDs)`; when `answersToShow` is empty,
`hasAllowedList` is `false` and the component falls back to unrestricted async search (no default
list shown at all) rather than proactively rendering "all" — the opposite failure mode. Do not
generalize this issue's fix to subject-search fields.
---
## Current workaround — the sentinel-UUID hack
The only known way today to force zero options on a coded or group-member field is to pass a
non-matching UUID into `showAnswers()`:
```js
statusBuilder.showAnswers('00000000-0000-0000-0000-000000000000'); // "no match" hack
```
This is **a caller convention, not a named constant** — a repo-wide grep for the literal string
`00000000-0000-0000-0000-000000000000` across `rules-config`, `avni-models`, and `avni-client`
returns zero matches outside the #38 issue text itself. There is no `NO_ANSWER_UUID` or similar
constant anywhere in the codebase; every rule author who needs this has to independently discover
and hand-roll the trick.
Why it renders zero options: pushing any single value into `answersToShow` makes it **non-empty**,
which flips both consumers above out of "show all" mode and into "filter to exactly this list" —
and since the sentinel matches no real concept name or subject UUID, the filtered result is empty.
Confirmed end-to-end for the group-member/OFF-toggle path via
`avni-webapp/src/dataEntryApp/components/SubjectFormElement.jsx:59-66`: a non-empty
`filteredAllowedUUIDs` array is passed to `api.fetchSubjectForUUIDs(...)`, which returns no
subjects for a UUID matching nothing, so `setAllowedOptions([])` renders an empty list.
---
## Proposed API — `showNoAnswers()`
```js
statusBuilder.showNoAnswers(); // explicit: render zero options
```
**This method does not exist yet** — a grep for `showNoAnswers` across `rules-config/src`,
`avni-models/src`, and `avni-client/src` returns zero matches. This is a net-new addition to
`rules-config/src/rules/builder/FormElementStatusBuilder.js`.
---
## Acceptance Criteria
1. `showNoAnswers()` renders zero options for coded and group-member (subject-multiselect) fields.
2. Behaviour requires "Display all group members" to be **OFF**. — **CONFIRMED**, with a corrected
mechanism vs. the original #38 wording: `displayAllGroupMembers` is a **form-element design-time
key-value** (`FormElement.keys.displayAllGroupMembers`, avni-models `FormElement.js:334`), not a
rule. It's read once by `avni-webapp/src/dataEntryApp/components/LandingSubjectFormElement.jsx:6-16`
to pick between two entirely different components:
- **ON** → renders `AttendanceFormElement`, which fetches *all* group members via
`api.fetchGroupMembers(subjectUUID)` (`AttendanceFormElement.jsx:68-81`) and **never reads
`answersToShow` at all** — so `showNoAnswers()` (or any rule-driven filter) has zero effect
while this toggle is ON.
- **OFF** → renders `SubjectFormElement` instead, which *does* read `formElement.answersToShow`
(`SubjectFormElement.jsx:22-24`) — this is the path `showNoAnswers()` actually affects.
So AC#2 is correct in substance (toggle must be OFF), but the reason is "OFF renders a
different, rule-aware component," not "the rule result gets overridden inside the same
component." Worth noting for the implementer: `AttendanceFormElement.jsx`'s own internal
`answersToShow`-reading branch (lines 82-87) appears unreachable in current code — the only
render call site (`LandingSubjectFormElement.jsx`) only mounts `AttendanceFormElement` when the
toggle is truthy, so that branch never runs today. Flagging as an observation, not asserting
dead-code removal is in scope for this issue.
3. Existing empty-list-means-all behaviour is documented in the `FormElementStatusBuilder` API
reference, with the sentinel noted as the legacy approach.
— **Doc-path correction:** `FORM_ELEMENT_BUILDER_API.md` is **not a rules-config file**. It
exists only as a generated snapshot at `avni-skills/product-codebase/FORM_ELEMENT_BUILDER_API.md`
(verified present; documents `skipAnswers()` at line 77 and `showAnswers()` at line 92, but does
**not** currently mention the empty-means-all footgun for either field type). `rules-config`
itself has only a `README.md` at repo root — no `FORM_ELEMENT_BUILDER_API.md` in-repo. **Open
question, not resolved here:** what is the canonical home for this doc — should rules-config gain
its own copy, or does the generated snapshot get regenerated from some other source of truth?
Don't merge this AC assuming an in-repo path exists today.
---
## Tech Approach
**Touchpoint:** `rules-config/src/rules/builder/FormElementStatusBuilder.js`.
- `showAnswers(...answers)` (line 46) pushes a `{rule, answers}` pair onto `this.answersToShow`
(an array accumulator, initialized at the constructor, line 12).
- `build()` (lines 62-73) reduces `this.answersToShow` down to a flat `answersToShow` array
(line 66) and — critically — **throws if both `answersToSkip` and `answersToShow` are non-empty**
(lines 68-69):
```js
if (answersToSkip.length > 0 && answersToShow.length > 0) {
throw Error(`Rule for FormElement '${this.context.formElement.name}' uses both skipAnswers and showAnswers.`);
}
```
Any `showNoAnswers()` implementation must thread through this same guard without tripping it —
i.e. it needs to produce a **non-empty** `answersToShow` result (to distinguish from "no rule
fired" / true emptiness) while still being mutually exclusive with `skipAnswers()`, exactly like
`showAnswers()` already is.
**Design fork — two viable implementations, different cross-repo footprints. This issue should
state which one is being chosen, not leave it ambiguous:**
- **Sentinel-emit** (minimal footprint): `showNoAnswers()` is sugar that pushes a reserved
non-matching value into the same `answersToShow` accumulator `showAnswers()` uses today (i.e.
formalizes the existing hack behind a named method). **Zero changes needed outside
rules-config** — every downstream consumer (avni-models `getAnswers()` /
`getApplicableSubjectUUIDs()`, and the webapp render components that read `answersToShow`
directly, e.g. `SubjectFormElement.jsx:22`) already treats "non-empty array matching nothing" as
"filter to empty," because that's exactly what the sentinel hack exploits today.
- **Real-flag** (clean semantics, wider footprint): add an explicit `noAnswers` boolean to
`FormElementStatus` (`rules-config/src/rules/model/FormElementStatus.js`) and thread it through
to a real `FormElement` field, then change `getAnswers()` (avni-models `FormElement.js:283-297`)
and `getApplicableSubjectUUIDs()` (`FormElement.js:303-306`) to check that flag before falling
into today's empty-means-all branch. **This requires an avni-models change**, and — because at
least one render component (`SubjectFormElement.jsx:22`) reads the raw `answersToShow` field
directly rather than calling the `getApplicable*()` getters — it may also require touching that
render component so it doesn't fall back to unrestricted search when the "real" flag is set but
the raw array is still empty. Not confirmed exhaustive; only one render path was checked.
Recommend picking **sentinel-emit** for a minimal, low-risk first cut (matches the "wrap the
existing hack in a discoverable name" spirit of #38's own proposed API), with the real-flag
approach as a documented follow-up if the sentinel value ever collides with a real UUID/concept
name in some org's data.
---
## Open questions / design decisions to resolve before dev
1. **Sentinel-emit vs. real-flag** (above) — pick one explicitly; do not leave the builder API
ambiguous about which consumers need touching.
2. **Canonical doc home for `FORM_ELEMENT_BUILDER_API.md`** — currently only a generated snapshot
under `avni-skills/product-codebase/`, not a rules-config source file. Decide where the
authoritative doc lives before AC#3 can be called done.
3. **`AttendanceFormElement.jsx`'s dead `answersToShow` branch** (lines 82-87, per AC#2 above) —
out of scope for this issue, but worth a follow-up ticket if confirmed unreachable, since it's
adjacent code that looks like it should matter for this feature but currently doesn't.
---
## Testing Gotchas
- Cover **both** field types explicitly — coded (`getAnswers()`) and group-member/subject-multiselect
(`getApplicableSubjectUUIDs()` / `SubjectFormElement.jsx`) — they're different code paths, not one
shared implementation.
- Group-member test must set the "Display all group members" toggle **OFF** as a precondition —
with it ON, `AttendanceFormElement` bypasses rule filtering entirely (see AC#2) and
`showNoAnswers()` will appear to silently do nothing.
- Regression check: confirm `showAnswers()` with a genuinely empty result (no rule fired, i.e. true
emptiness, not `showNoAnswers()`'s output) still means "show all" for existing rules — this
feature must not change that default for anyone who hasn't opted into `showNoAnswers()`.
- Confirm the mutual-exclusion guard in `build()` (`FormElementStatusBuilder.js:68-69`) still fires
correctly if a rule author combines `skipAnswers()` and `showNoAnswers()` on the same field.
- Do not test subject-search (plain, non-group) fields as part of this fix — their empty semantics
are already the opposite (unrestricted search, not "show all") and are out of scope.
---
## Visual Design
No UI changes. This is a rule-library API addition plus documentation — the client/webapp render
components are unchanged (they already correctly render "zero options" whenever `answersToShow` is
a non-empty array that matches nothing; `showNoAnswers()` only changes how a rule author produces
that array).
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with rules-config/src/rules/builder/FormElementStatusBuilder.js and the referenced FormElementStatus.js, avni-models FormElement.js, and webapp consumers. Resolve the sentinel-versus-real-flag design and canonical documentation location before implementation; done means the chosen approach satisfies the coded and group-member acceptance criteria and documents the empty-list behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- api, developer-experience
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100