Repository navigation
feat: wire HIDEOPTION/HIDEOPTIONGROUP/SHOWOPTIONGROUP rule actions end-to-end - #19
Conversation
…d-to-end The rule engine already computed hiddenOptions/hiddenOptionGroups per field, but nothing consumed them: useFieldControl never surfaced them and widgets always rendered the full static option list. OptionGroup membership was also never fetched anywhere, so group-scoped hides had no way to resolve to concrete option codes. Adds an optionGroups fetch (metadata query + resolver), a shared resolveHiddenOptionCodes helper, FieldControlReturn.visibleOptions for widgets to render, and a filterPayload guard so a submitted value pointing at a now-hidden option/group member is nulled out at submit time. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe change adds option-group metadata queries and resolution, threads the resulting map through playground forms and form stores, filters hidden options from controls, and nulls hidden option values during payload submission. Tests and architecture documentation cover the new behavior. ChangesOption-group metadata and rule resolution
Form controls and playground integration
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
packages/metadata/src/optionGroups.ts (1)
7-14: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winDerive raw option-group types from a Zod schema.
RawOptionGroupandRawOptionGroupsResultare hand-written interfaces that describe an external query response. They are not derived from a Zod schema withz.infer<>, so a malformed or unexpected shape from theoptionGroupsQueryresponse is not validated at runtime. Optional chaining only guards againstundefined, not against wrong types.Define a Zod schema for the option-group response shape and derive both types with
z.infer<>, consistent with the metadata package's schema-driven approach.♻️ Proposed refactor using a Zod schema
-type RawOptionGroup = { - id?: string; - options?: Array<{ code?: string }>; -}; - -export type RawOptionGroupsResult = { - optionGroups?: { optionGroups?: RawOptionGroup[] }; -}; +const rawOptionGroupSchema = z.object({ + id: z.string().optional(), + options: z.array(z.object({ code: z.string().optional() })).optional(), +}); + +const rawOptionGroupsResultSchema = z.object({ + optionGroups: z + .object({ optionGroups: z.array(rawOptionGroupSchema).optional() }) + .optional(), +}); + +export type RawOptionGroupsResult = z.infer<typeof rawOptionGroupsResultSchema>;As per coding guidelines, "Use TypeScript strict mode, derive types from Zod schemas with
z.infer<>, and do not useany."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/metadata/src/optionGroups.ts` around lines 7 - 14, Replace the hand-written RawOptionGroup and RawOptionGroupsResult definitions with a Zod schema describing the nested optionGroups response, including optional ids, option codes, and arrays. Derive both exported types with z.infer<> from the schema, and use the schema in the optionGroupsQuery response validation path so malformed shapes are rejected at runtime without introducing any.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/playground/src/hooks/useOptionGroupsSupplementaryData.ts`:
- Around line 36-42: Update the form-rendering flow around the supplementary
option-group hook so tracker/event forms are not initialized or rendered until
the option-group query has returned data. Propagate and display an explicit
query failure from the hook instead of treating missing metadata as an empty
map, while preserving the existing resolveOptionGroups behavior after successful
loading.
In `@docs/ARCHITECTURE.md`:
- Around line 143-153: Update docs/ARCHITECTURE.md lines 143-153 and
docs/use-tracker-form.md lines 450-456 and 489-492 to consistently document
optional option-group metadata: declare or obtain optionGroups where needed and
pass it as the third argument to filterPayload(values, fieldState,
optionGroups). Align the examples with useTrackerForm(options.optionGroups) and
preserve the existing two-argument behavior when no metadata is available.
In `@packages/metadata/src/queries/optionGroups.query.ts`:
- Around line 15-19: Update optionGroupsQuery.params to validate that
variables.optionGroupIds is present before calling join, using a typed
QueryVariables contract or runtime guard; preserve the existing filter
construction for valid IDs and provide a clear failure or safe result when
external callers omit the variable.
In `@utils/hooks/src/formStore.ts`:
- Around line 36-39: Update utils/hooks/src/formStore.ts:36-39 around
setOptionGroups to maintain an externally subscribable option-group store and
notify listeners when the value changes. In
utils/hooks/src/useEventForm.ts:100-116 and
utils/hooks/src/useTrackerForm.ts:66-82, route changed or asynchronously loaded
options.optionGroups through this update path. In
utils/hooks/src/fields/useFieldControl.ts:56-65, subscribe to the option-group
store before filtering options so visibleOptions recomputes for existing
controls.
In `@utils/rules/src/filterPayload.ts`:
- Around line 23-27: Update the hidden-option handling in filterPayload to
process delimited multi-select strings, removing each code present in
resolveHiddenOptionCodes(state, optionGroups) while preserving visible codes and
the existing null behavior for values that become empty. Keep exact single-code
handling intact and use the field’s established delimiter/list format.
---
Nitpick comments:
In `@packages/metadata/src/optionGroups.ts`:
- Around line 7-14: Replace the hand-written RawOptionGroup and
RawOptionGroupsResult definitions with a Zod schema describing the nested
optionGroups response, including optional ids, option codes, and arrays. Derive
both exported types with z.infer<> from the schema, and use the schema in the
optionGroupsQuery response validation path so malformed shapes are rejected at
runtime without introducing any.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: da53c22b-f9b6-4d1c-9962-e62fb4f11f50
📒 Files selected for processing (31)
apps/playground/src/components/programs/forms/ProgramEventForm.tsxapps/playground/src/components/programs/forms/ProgramRegistrationForm.tsxapps/playground/src/components/programs/forms/ProgramRegistrationFormScreen.tsxapps/playground/src/components/programs/forms/ProgramStageFormScreen.tsxapps/playground/src/components/programs/forms/TrackerProgramShell.tsxapps/playground/src/hooks/useOptionGroupsSupplementaryData.tsapps/playground/src/pages/ProgramPage.test.tsxapps/playground/src/pages/ProgramPage.tsxcomponents/dhis2-ui/src/fields/widgets/ChoiceFields.tsxcomponents/mantine/src/fields/widgets/ChoiceFields.tsxcomponents/mui/src/fields/widgets/ChoiceFields.tsxdocs/ARCHITECTURE.mddocs/form-state-architecture.mddocs/use-field-control-plan.mddocs/use-tracker-form.mdpackages/metadata/src/index.tspackages/metadata/src/optionGroups.test.tspackages/metadata/src/optionGroups.tspackages/metadata/src/queries/optionGroups.query.test.tspackages/metadata/src/queries/optionGroups.query.tsutils/hooks/src/fields/useFieldControl.test.tsutils/hooks/src/fields/useFieldControl.tsutils/hooks/src/formStore.tsutils/hooks/src/test/renderFieldControl.tsxutils/hooks/src/useEventForm.tsutils/hooks/src/useTrackerForm.tsutils/rules/src/filterPayload.test.tsutils/rules/src/filterPayload.tsutils/rules/src/index.tsutils/rules/src/resolveHiddenOptionCodes.test.tsutils/rules/src/resolveHiddenOptionCodes.ts
| useEffect(() => { | ||
| if (optionGroupIds.length > 0) { | ||
| refetch({ optionGroupIds }) | ||
| } | ||
| }, [optionGroupIds, refetch]) | ||
|
|
||
| return useMemo(() => (data ? resolveOptionGroups(data) : undefined), [data]) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect option-group readiness and propagation without executing repository code.
ast-grep outline apps/playground/src/hooks/useOptionGroupsSupplementaryData.ts --items all
ast-grep outline utils/hooks/src --items all --type function --match 'useEventForm|useTrackerForm'
# Verify how undefined optionGroups reaches form initialization and payload submission.
rg -n -C 4 '\boptionGroups\b|filterPayload|handleSubmit|onSubmit' \
apps/playground/src utils/hooks/src utils/rules/srcRepository: nnkogift/dhis2-form-utils
Length of output: 50381
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== hook implementation =="
cat -n apps/playground/src/hooks/useOptionGroupsSupplementaryData.ts
echo "== ProgramPage relevant sections =="
sed -n '1,190p' apps/playground/src/pages/ProgramPage.tsx | cat -n
echo "== ProgramEventForm submission relevant sections =="
sed -n '48,160p' apps/playground/src/components/programs/forms/ProgramEventForm.tsx | cat -n
echo "== ProgramRegistrationForm submission relevant sections =="
sed -n '60,155p' apps/playground/src/components/programs/forms/ProgramRegistrationForm.tsx | cat -n
echo "== resolveHiddenOptionCodes and filterPayload implementations =="
cat -n utils/rules/src/resolveHiddenOptionCodes.ts
cat -n utils/rules/src/filterPayload.ts
echo "== tracker and event form optionGroups assignment =="
sed -n '55,85p' utils/hooks/src/useTrackerForm.ts | cat -n
sed -n '95,112p' utils/hooks/src/useEventForm.ts | cat -n
echo "== formStore optionGroups getters/setters =="
sed -n '1,50p' utils/hooks/src/formStore.ts | cat -nRepository: nnkogift/dhis2-form-utils
Length of output: 25895
Block form rendering until option-group metadata resolves.
OptionGroupCodeMap | undefined is passed into useEventForm/useTrackerForm, which installs it on the form store. resolveHiddenOptionCodes and filterPayload skip hidden option-group members when optionGroups is absent, so HIDEOPTIONGROUP rules do not hide option values or strip them from the payload. Delay the tracker/event form until the query returns data, and report explicit query failures.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/playground/src/hooks/useOptionGroupsSupplementaryData.ts` around lines
36 - 42, Update the form-rendering flow around the supplementary option-group
hook so tracker/event forms are not initialized or rendered until the
option-group query has returned data. Propagate and display an explicit query
failure from the hook instead of treating missing metadata as an empty map,
while preserving the existing resolveOptionGroups behavior after successful
loading.
| `hiddenOptions` holds option codes hidden directly by `HIDEOPTION`; `hiddenOptionGroups` holds | ||
| optionGroup ids hidden by `HIDEOPTIONGROUP` — group _membership_ (which option codes belong to a | ||
| group) is resolved separately, since it requires an `optionGroups` API fetch the metadata package | ||
| doesn't do on its own. Callers fetch it (e.g. via `extractReferencedOptionGroupIds` + | ||
| `optionGroupsQuery` + `resolveOptionGroups` from `@dhis2-form-utils/metadata`) and pass it as the | ||
| `optionGroups` option to `useEventForm`/`useTrackerForm`. `resolveHiddenOptionCodes` (in | ||
| `@dhis2-form-utils/rules`) unions `hiddenOptions` with the resolved group members into a single | ||
| `Set<string>` of hidden codes — `useFieldControl` uses it to compute `FieldControlReturn.visibleOptions` | ||
| for widgets to render, and `filterPayload`'s optional third argument uses it to null out a submitted | ||
| value that references a now-hidden option. | ||
|
|
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 6 \
'UseTrackerFormOptions|optionGroups|filterPayload\s*\(' \
. --glob '*.ts' --glob '*.tsx' --glob '*.md'Repository: nnkogift/dhis2-form-utils
Length of output: 50383
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== utils/rules/src/filterPayload.ts =="
fd -a 'filterPayload\.ts$' . | while read -r f; do
rel="${f#${PWD}/}"
echo "--- $rel"
cat -n "$rel"
done
echo "== docs/ARCHITECTURE.md relevant sections =="
sed -n '135,160p;222,233p;564,576p' docs/ARCHITECTURE.md | cat -n
echo "== docs/use-tracker-form.md relevant sections =="
sed -n '358,460p;484,496p' docs/use-tracker-form.md | cat -n
echo "== export tests/data for filterPayload optionGroups =="
rg -n -C 5 'filterPayload\(|optionGroups' utils/rules src packages --glob '*.ts' --glob '*.tsx' --glob '*.md' \
| head -220Repository: nnkogift/dhis2-form-utils
Length of output: 27124
Align the option-group contract across the documentation.
The implementation supports filterPayload(values, fieldState, optionGroups?) and useTrackerForm(options.optionGroups), but the docs split the contract: use-tracker-form.md passes formStore.optionGroups, while docs/ARCHITECTURE.md documents filterPayload as two args and the submission example omits option groups. Update both doc sites so callers declare optional option-group metadata and pass it to filterPayload consistently.
📍 Affects 2 files
docs/ARCHITECTURE.md#L143-L153(this comment)docs/use-tracker-form.md#L450-L456docs/use-tracker-form.md#L489-L492
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/ARCHITECTURE.md` around lines 143 - 153, Update docs/ARCHITECTURE.md
lines 143-153 and docs/use-tracker-form.md lines 450-456 and 489-492 to
consistently document optional option-group metadata: declare or obtain
optionGroups where needed and pass it as the third argument to
filterPayload(values, fieldState, optionGroups). Align the examples with
useTrackerForm(options.optionGroups) and preserve the existing two-argument
behavior when no metadata is available.
| params: (variables: QueryVariables) => ({ | ||
| fields: OPTION_GROUP_FIELDS, | ||
| filter: `id:in:[${(variables.optionGroupIds as string[]).join(',')}]`, | ||
| paging: false, | ||
| }), |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Trace callers of optionGroupsQuery to check for an empty-ids guard.
rg -n -C6 'optionGroupsQuery' --type=tsRepository: nnkogift/dhis2-form-utils
Length of output: 163
🏁 Script executed:
#!/bin/bash
set -u
echo "Tracked files matching option group query:"
git ls-files | rg 'optionGroups\.query\.ts|optiongroup|optionGroup' || true
echo
echo "Search all tracked files for optionGroupsQuery:"
rg -n -C4 'optionGroupsQuery|extractReferencedOptionGroupIds|OPTION_GROUP_FIELDS|optionGroupIds' . || trueRepository: nnkogift/dhis2-form-utils
Length of output: 15899
Handle missing optionGroupIds before building the filter.
optionGroupsQuery.params() asserts variables.optionGroupIds as string[] without a guard. If a consumer calls this query without that variable, it throws at runtime. Use a typed variable contract or runtime validation before .join(',').
The Playground hook skips refetching when optionGroupIds is empty, so the empty-optionGroupIds path is covered there; external callers should avoid invoking the query with no ids.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/metadata/src/queries/optionGroups.query.ts` around lines 15 - 19,
Update optionGroupsQuery.params to validate that variables.optionGroupIds is
present before calling join, using a typed QueryVariables contract or runtime
guard; preserve the existing filter construction for valid IDs and provide a
clear failure or safe result when external callers omit the variable.
| /** Sets option-group membership directly, without going through `init`/`reinit`. */ | ||
| setOptionGroups(optionGroups: OptionGroupCodeMap | undefined): void { | ||
| this.optionGroupsRef = { current: optionGroups }; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 8 \
'setOptionGroups|optionGroupsRef\.current|useFormStore|useFieldState|visibleOptions' \
utils/hooks/src apps/playground/srcRepository: nnkogift/dhis2-form-utils
Length of output: 22164
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '--- locate key files ---\n'
fd -a 'FormStore|FormStateContext|useEventForm|useTrackerForm|useFieldControl|resolveHiddenOptionCodes|rules' . | sed 's#^\./##' | sort | head -80
printf '\n--- formStore.ts ---\n'
cat -n utils/hooks/src/formStore.ts
printf '\n--- FieldStateContext.tsx relevant ---\n'
sed -n '1,140p' utils/hooks/src/FormStateContext.tsx | cat -n
printf '\n--- useEventForm relevant ---\n'
sed -n '80,125p' utils/hooks/src/useEventForm.ts | cat -n
printf '\n--- useTrackerForm relevant ---\n'
sed -n '55,90p' utils/hooks/src/useTrackerForm.ts | cat -n
printf '\n--- fields/useFieldControl.ts ---\n'
sed -n '44,86p' utils/hooks/src/fields/useFieldControl.ts | cat -n
printf '\n--- playground optionGroups references ---\n'
rg -n -C 6 'optionGroups|setOptionGroups|useEventForm|useTrackerForm' apps/playground/src || true
printf '\n--- rule effect/hidden option helpers ---\n'
rg -n -C 6 'function resolveHiddenOptionCodes|resolveHiddenOptionCodes|hiddenOptionGroups|HIDEOPTIONGROUP' . --glob '!package-lock.json' --glob '!node_modules/**'Repository: nnkogift/dhis2-form-utils
Length of output: 254
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- locate key files ---'
fd -a 'FormStore|FormStateContext|useEventForm|useTrackerForm|useFieldControl|resolveHiddenOptionCodes|rules' . | sed 's#^\./##' | sort | head -80
printf '%s\n' ''
printf '%s\n' '--- formStore.ts ---'
cat -n utils/hooks/src/formStore.ts
printf '%s\n' ''
printf '%s\n' '--- FormStateContext.tsx relevant ---'
sed -n '1,140p' utils/hooks/src/FormStateContext.tsx | cat -n
printf '%s\n' ''
printf '%s\n' '--- useEventForm relevant ---'
sed -n '80,125p' utils/hooks/src/useEventForm.ts | cat -n
printf '%s\n' ''
printf '%s\n' '--- useTrackerForm relevant ---'
sed -n '55,90p' utils/hooks/src/useTrackerForm.ts | cat -n
printf '%s\n' ''
printf '%s\n' '--- fields/useFieldControl.ts ---'
sed -n '44,86p' utils/hooks/src/fields/useFieldControl.ts | cat -n
printf '%s\n他' ''
printf '%s\n' '--- playground optionGroups references ---'
rg -n -C 6 'optionGroups|setOptionGroups|useEventForm|useTrackerForm' apps/playground/src || true
printf '%s\n' ''
printf '%s\n' '--- hidden option helpers ---'
rg -n -C 6 'function resolveHiddenOptionCodes|resolveHiddenOptionCodes|hiddenOptionGroups|HIDEOPTIONGROUP' . --glob '!package-lock.json' --glob '!node_modules/**'Repository: nnkogift/dhis2-form-utils
Length of output: 50382
Subscribe to option-group changes in field controls.
useFieldControl reads formStore.optionGroups through a plain getter, so an async or changed options.optionGroups in useEventForm/useTrackerForm does not make existing controls recompute visibleOptions unless the engine reinitializes. Add an external-stored option-group update path that useFieldControl subscribes to before filtering options.
📍 Affects 4 files
utils/hooks/src/formStore.ts#L36-L39(this comment)utils/hooks/src/useEventForm.ts#L100-L116utils/hooks/src/useTrackerForm.ts#L66-L82utils/hooks/src/fields/useFieldControl.ts#L56-L65
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@utils/hooks/src/formStore.ts` around lines 36 - 39, Update
utils/hooks/src/formStore.ts:36-39 around setOptionGroups to maintain an
externally subscribable option-group store and notify listeners when the value
changes. In utils/hooks/src/useEventForm.ts:100-116 and
utils/hooks/src/useTrackerForm.ts:66-82, route changed or asynchronously loaded
options.optionGroups through this update path. In
utils/hooks/src/fields/useFieldControl.ts:56-65, subscribe to the option-group
store before filtering options so visibleOptions recomputes for existing
controls.
Source: Coding guidelines
| } else if ( | ||
| typeof value === 'string' && | ||
| resolveHiddenOptionCodes(state, optionGroups).has(value) | ||
| ) { | ||
| result[key] = null; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check how multi-value option fields (if any) are represented in FieldState/values.
rg -n -C5 'MULTI_TEXT|multiSelect|multi-select' --type=tsRepository: nnkogift/dhis2-form-utils
Length of output: 163
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Tracked files under utils/rules:"
git ls-files utils/rules | sed -n '1,120p'
echo
echo "filterPayload.ts and nearby files:"
fd -a '^(filterPayload|.*Option.*|.*Rule.*)\.ts$|package.json' utils/rules | sed -n '1,120p'
echo
echo "Search option field symbols:"
rg -n -C3 'OPTION|option|hidden|MULTI|text|Multi|Array<|Set<|split\(|optionGroups|resolveHiddenOptionCodes' --type=ts --type=tso --type=tsx . | sed -n '1,240p'Repository: nnkogift/dhis2-form-utils
Length of output: 1176
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "filterPayload.ts:"
cat -n utils/rules/src/filterPayload.ts
echo
echo "filterPayload tests:"
cat -n utils/rules/src/filterPayload.test.ts
echo
echo "resolveHiddenOptionCodes.ts and tests:"
cat -n utils/rules/src/resolveHiddenOptionCodes.ts
echo
cat -n utils/rules/src/resolveHiddenOptionCodes.test.ts
echo
echo "types.ts:"
cat -n utils/rules/src/types.ts
echo
echo "ruleValueType.ts:"
cat -n utils/rules/src/ruleValueType.ts
echo
echo "Search relevant symbols in utils/rules:"
rg -n -C3 'OPTION|option|hidden|MULTI|text|Multi|Array<|Set<|split\(|optionGroups|resolveHiddenOptionCodes|filterPayload' utils/rules --type=ts --type=tsx | sed -n '1,320p'Repository: nnkogift/dhis2-form-utils
Length of output: 9893
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Workspace metadata package candidates:"
git ls-files | rg '(^|/)metadata|package.json$' | sed -n '1,200p'
echo
echo "Search for field value types/options in repo:"
rg -n -C4 'FieldState|Multi|multi|MULTI|OPTION|Option|optionGroups|filterPayload|hiddenOptions' . --glob '*.ts' --glob '*.tsx' | sed -n '1,400p'
echo
echo "Inspect package manifests mentioning metadata/form-utils:"
rg -n -C3 '`@dhis2-form-utils/metadata`|utils/rules|peerDependencies|dependencies' package.json pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null | sed -n '1,240p' || trueRepository: nnkogift/dhis2-form-utils
Length of output: 43869
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Metadata schema files (short outlines/partial contents):"
for f in packages/metadata/src/buildSchema.ts packages/metadata/src/buildTrackerSchema.ts packages/metadata/src/enums.ts packages/metadata/src/types.ts; do
echo "--- $f"
rg -n 'multiText|MULTI_TEXT|MultiText|optionSet|OPTION|Text|z\.string|z\.array|z\.enum|z\.union|multiSelect|filterPayload' "$f" -C 5
echo
done
echo "Tests mentioning filterPayload only in TS files:"
rg -n -C4 'filterPayload|hiddenOptions|multiText|MULTI_TEXT' utils/packages hooks packages --glob '*.ts' --glob '*.tsx' || true
echo
echo "Read-only behavioral probe for current filterPayload branch:"
node - <<'JS'
const hiddenCodes = new Set(['hidden']);
function current(value) {
return typeof value === 'string' && hiddenCodes.has(value) ? null : value;
}
const inputs = ['hidden', 'hidden,visible', 'visible,hidden', ['hidden', 'visible'], 'hidden visible'];
for (const input of inputs) {
console.log(JSON.stringify(input), '=>', JSON.stringify(current(input)));
}
JSRepository: nnkogift/dhis2-form-utils
Length of output: 3055
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Rest of buildSchema.ts:"
cat -n packages/metadata/src/buildSchema.ts | sed -n '1,160p'
echo
echo "Rest of types.ts:"
cat -n packages/metadata/src/types.ts | sed -n '1,220p'
echo
echo "Rest of buildTrackerSchema.ts:"
cat -n packages/metadata/src/buildTrackerSchema.ts | sed -n '1,180p'
echo
echo "Rest of buildTeaFieldSchema.ts:"
cat -n packages/metadata/src/buildTeaFieldSchema.ts | sed -n '1,220p'Repository: nnkogift/dhis2-form-utils
Length of output: 8954
🌐 Web query:
DHIR2 option set multi text field delimiter selected value option code
💡 Result:
In DHIS2, multi-select functionality for option sets is implemented using the MULTI_TEXT value type [1][2][3]. 1. Implementation: To enable multi-selection, you must configure a data element with the value type MULTI_TEXT and associate it with an option set that also supports multiple values [2][3]. 2. Delimiter: When multiple options are selected, they are stored as sequential values within the field, separated by a delimiter [1]. While historically associated with a semi-colon (;) in many DHIS2 internal structures [1][4][5], the system handles these as a collection of values linked to the specific option codes. 3. Option Codes: Data elements with option sets typically save the selected options using their defined option codes, not the display names [6][7][8][9]. When using multi-select (MULTI_TEXT), the field stores multiple such codes combined together [1][10]. 4. Compatibility: Support for multi-select option sets was introduced for aggregate data in version 2.40 and expanded to include Tracker programs in version 2.41 [3][11]. 5. Program Rules and Analysis: Because these fields store multiple values, referencing them in program rules or indicators requires specific handling, such as using regex-based functions like d2:validatePattern to check for the presence of a specific option code within the stored string [10]. For program indicators, ensure you are using the unique option code defined in your option set when building expressions [8].
Citations:
- 1: https://archive.lists.launchpad.net/dhis2-devs/msg47005.html
- 2: https://community.dhis2.org/t/selecting-multiple-answers-on-options-sets/59088
- 3: https://community.dhis2.org/t/checkbox-in-dhis2-individual-data/66887/1
- 4: https://docs.dhis2.org/en/develop/using-the-api/dhis-core-version-240/data.html
- 5: https://archive.lists.launchpad.net/dhis2-devs/msg33265.html
- 6: https://archive.lists.launchpad.net/dhis2-devs/msg32481.html
- 7: https://archive.lists.launchpad.net/dhis2-devs/msg49260.html
- 8: https://community.dhis2.org/t/can-i-have-a-program-indicator-that-has-categories-based-on-an-optionset/46276
- 9: https://docs.dhis2.org/en/implement/database-design/tracker-system-design/program-stages-structure.html
- 10: https://community.dhis2.org/t/defining-program-rules-for-multiselect-questions-in-dhis2/62938
- 11: https://community.dhis2.org/t/creating-program-indicators-based-on-multiselect-data-element-in-dhis2-v41-capture-app/64589
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "ProgramStageDataElement/optionSet type definitions in fieldFilters.ts:"
fd -a 'fieldFilters.ts' . | xargs -r -n1 sh -c 'echo "--- $0"; rg -n -C8 "ProgramStageDataElement|optionSet|valueType" "$0"'
echo
echo "Repository search for delimiter values:"
rg -n -C3 'semi-colon|semicolon|delimiter|multiText|MULTI_TEXT|;|split\(' --glob '*.ts' --glob '*.tsx' . | sed -n '1,260p'Repository: nnkogift/dhis2-form-utils
Length of output: 23693
🌐 Web query:
DHIS2 Tracker program option set MULTI_TEXT delimiter value stored API comma semicolon
💡 Result:
In DHIS2, the MULTI_TEXT value type allows for the selection of multiple options from an option set within a single data element or attribute [1][2]. This feature was introduced for aggregate data in version 2.40 [3][2] and expanded to support Tracker programs starting in version 2.41 [3][4]. When multiple options are selected, they are stored as a single string of concatenated values [5]. These values are typically separated by a comma (,) [6][5]. While older conceptual discussions mentioned the potential use of a semicolon [7][8], comma-separated strings are the standard implementation observed in current Tracker and Aggregate data output [6][5]. Key considerations for MULTI_TEXT implementation include: 1. Data Storage: The system stores the codes of the selected options, not the display names [5]. These codes are stored as a delimited string (e.g., "1,3,2") reflecting the order in which the user selected them, rather than a pre-sorted numeric order [5]. 2. Program Rules: Because the values are stored as a single string, standard equality operators (e.g., #{variable} == 'OptionCode') will not work for multi-select fields [6]. Instead, developers must use the d2:validatePattern function with a regular expression (e.g., d2:validatePattern(Variable, '.\bOptionCode\b.')) to check if a specific option is present within the string [6][5]. 3. Reporting and Analysis: Users have reported challenges in analytics and reporting apps (such as the Line Listing app), where the system may display the raw codes (e.g., "1,3") instead of the human-readable option names when multiple values are selected [5]. For metadata imports, ensure that the valueType is explicitly set to MULTI_TEXT in your import files to ensure the system treats the field as a multi-select rather than a standard text field [9].
Citations:
- 1: https://docs.dhis2.org/en/use/android-app/value-types-supported.html
- 2: https://github.com/dhis2/dhis2-releases/blob/master/releases/2.40/ReleaseNote-2.40.md
- 3: https://community.dhis2.org/t/checkbox-in-dhis2-individual-data/66887
- 4: https://community.dhis2.org/t/creating-program-indicators-based-on-multiselect-data-element-in-dhis2-v41-capture-app/64589
- 5: https://community.dhis2.org/t/feedback-challenges-with-the-new-multiple-choice-option-set-in-dhis2/65061
- 6: https://community.dhis2.org/t/defining-program-rules-for-multiselect-questions-in-dhis2/62938
- 7: https://archive.lists.launchpad.net/dhis2-devs/msg47005.html
- 8: https://community.dhis2.org/t/multi-select-dropdown-or-multi-select-checkbox-in-tracker-capture-forms/3812
- 9: https://community.dhis2.org/t/selecting-multiple-answers-on-options-sets/59088
Handle hidden option codes in delimited multi-select values.
MULTI_TEXT fields can be valid payload values, and DHIS2 stores multi-selected option codes as a delimited string, not an exact option code. An exact resolveHiddenOptionCodes(...).has(value) match leaves codes like opt1,hidden,2 unchanged when hidden is a rule-hidden option. Strip each hidden code from delimited/list values, or narrow the contract if multi-select option fields are out of scope here.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@utils/rules/src/filterPayload.ts` around lines 23 - 27, Update the
hidden-option handling in filterPayload to process delimited multi-select
strings, removing each code present in resolveHiddenOptionCodes(state,
optionGroups) while preserving visible codes and the existing null behavior for
values that become empty. Keep exact single-code handling intact and use the
field’s established delimiter/list format.


Summary
The rule engine (
utils/rules/src/evaluate.ts) already fully computedFieldState.hiddenOptions/hiddenOptionGroupsforHIDEOPTION/SHOWOPTION/HIDEOPTIONGROUP/SHOWOPTIONGROUPactions, but nothing downstream consumed them —useFieldControlnever surfaced them and every UI adapter widget rendered the full static option list. OptionGroup membership (which option codes belong to a group) was also never fetched anywhere, so group-scoped hides had no way to resolve to concrete option codes.This closes that gap end-to-end:
packages/metadata— newextractReferencedOptionGroupIds,optionGroupsQuery,resolveOptionGroups, andOptionGroupCodeMaptype for fetching/resolving option-group membership, scoped to only the groups a program's rules actually reference.utils/rules— newresolveHiddenOptionCodeshelper (unions directhiddenOptionswith resolved group members);filterPayloadgains an optional thirdoptionGroupsargument and now nulls out a submitted value that matches a hidden option/group member.utils/hooks—optionGroupsthreaded throughFormStore/useEventForm/useTrackerForm;useFieldControlnow exposesFieldControlReturn.visibleOptions— the option set filtered by live rule state.dhis2-ui,mantine,mui) —D2SelectFieldrenderscontrol.visibleOptionsinstead of the static option list.useOptionGroupsSupplementaryDatahook wired throughProgramPage→TrackerProgramShell/screens → forms, including thefilterPayloadsubmit-time guard.ARCHITECTURE.md,form-state-architecture.md,use-tracker-form.md,use-field-control-plan.mdupdated to describe the new flow.Test plan
pnpm typecheck— all 10 workspace packages cleanpnpm test— 187 unit tests + 80 Storybook browser tests passpnpm lint— clean (aside from pre-existing, unrelated warnings)PRT — Event Program Rules Test):HIDEOPTION(triggerhidered) → "Red" removed from the Colour dropdownHIDEOPTIONGROUP(triggerhidewarm) → "Red" + "Yellow" (warm colours group) removedSHOWOPTIONGROUP(triggershowwarm) → all four options restoredoptionGroupsAPI call fires (GET /api/optionGroups?filter=id:in:[...])PRT — Tracker Program Rules Test🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Tests