Skip to content

feat: wire HIDEOPTION/HIDEOPTIONGROUP/SHOWOPTIONGROUP rule actions end-to-end - #19

Merged
nnkogift merged 1 commit into
mainfrom
feat/hideoption-hideoptiongroup-support
Aug 3, 2026
Merged

nnkogift merged 1 commit into
mainfrom
feat/hideoption-hideoptiongroup-support

Conversation

@nnkogift

@nnkogift nnkogift commented Aug 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

The rule engine (utils/rules/src/evaluate.ts) already fully computed FieldState.hiddenOptions/hiddenOptionGroups for HIDEOPTION/SHOWOPTION/HIDEOPTIONGROUP/SHOWOPTIONGROUP actions, but nothing downstream consumed them — useFieldControl never 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 — new extractReferencedOptionGroupIds, optionGroupsQuery, resolveOptionGroups, and OptionGroupCodeMap type for fetching/resolving option-group membership, scoped to only the groups a program's rules actually reference.
  • utils/rules — new resolveHiddenOptionCodes helper (unions direct hiddenOptions with resolved group members); filterPayload gains an optional third optionGroups argument and now nulls out a submitted value that matches a hidden option/group member.
  • utils/hooks — optionGroups threaded through FormStore/useEventForm/useTrackerForm; useFieldControl now exposes FieldControlReturn.visibleOptions — the option set filtered by live rule state.
  • UI adapters (dhis2-ui, mantine, mui) — D2SelectField renders control.visibleOptions instead of the static option list.
  • Playground — new useOptionGroupsSupplementaryData hook wired through ProgramPage → TrackerProgramShell/screens → forms, including the filterPayload submit-time guard.
  • Docs — ARCHITECTURE.md, form-state-architecture.md, use-tracker-form.md, use-field-control-plan.md updated to describe the new flow.

Test plan

  • pnpm typecheck — all 10 workspace packages clean
  • pnpm test — 187 unit tests + 80 Storybook browser tests pass
  • pnpm lint — clean (aside from pre-existing, unrelated warnings)
  • Manually verified live against the DHIS2 demo instance (PRT — Event Program Rules Test):
    • HIDEOPTION (trigger hidered) → "Red" removed from the Colour dropdown
    • HIDEOPTIONGROUP (trigger hidewarm) → "Red" + "Yellow" (warm colours group) removed
    • SHOWOPTIONGROUP (trigger showwarm) → all four options restored
    • Confirmed the optionGroups API call fires (GET /api/optionGroups?filter=id:in:[...])
    • Confirmed no regressions against PRT — Tracker Program Rules Test

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added support for option-group metadata in program forms.
    • Fields now hide options dynamically based on rules, including options belonging to hidden groups.
    • Hidden option values are excluded or cleared from submitted form data.
  • Bug Fixes

    • Select fields now display the correct rule-filtered options.
  • Documentation

    • Documented option-group handling, hidden options, and submission behavior.
  • Tests

    • Added coverage for option-group resolution, filtered options, and payload handling.

…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>
@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

Option-group metadata and rule resolution

Layer / File(s) Summary
Metadata contracts and query
packages/metadata/src/optionGroups.ts, packages/metadata/src/queries/optionGroups.query.ts, packages/metadata/src/index.ts, packages/metadata/src/*.test.ts
Adds option-group types, rule-reference extraction, raw-result resolution, public exports, and an unpaged metadata query.
Hidden option-code filtering
utils/rules/src/resolveHiddenOptionCodes.ts, utils/rules/src/filterPayload.ts, utils/rules/src/index.ts, utils/rules/src/*.test.ts
Combines direct and grouped hidden option codes. filterPayload nulls hidden option values while preserving existing hidden-field and assigned-value handling.

Form controls and playground integration

Layer / File(s) Summary
Form state and visible controls
utils/hooks/src/formStore.ts, utils/hooks/src/useEventForm.ts, utils/hooks/src/useTrackerForm.ts, utils/hooks/src/fields/*, utils/hooks/src/test/renderFieldControl.tsx, components/*/src/fields/widgets/ChoiceFields.tsx
Stores option-group metadata, exposes visibleOptions, and updates select widgets to use filtered options.
Playground data flow
apps/playground/src/hooks/useOptionGroupsSupplementaryData.ts, apps/playground/src/pages/ProgramPage.tsx, apps/playground/src/pages/ProgramPage.test.tsx, apps/playground/src/components/programs/forms/*
Loads option groups referenced by program rules and forwards them through event and tracker form components to rendering and submission.
Documentation updates
docs/ARCHITECTURE.md, docs/form-state-architecture.md, docs/use-field-control-plan.md, docs/use-tracker-form.md
Describes option-group fetching, hidden-code resolution, visible options, and payload filtering.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the end-to-end wiring of the option rule actions covered by the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/hideoption-hideoptiongroup-support

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved: Cursor Security Agent passed with no findings requiring human review; Cursor Bugbot was not present on this PR. No reviewers assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
packages/metadata/src/optionGroups.ts (1)

7-14: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Derive raw option-group types from a Zod schema.

RawOptionGroup and RawOptionGroupsResult are hand-written interfaces that describe an external query response. They are not derived from a Zod schema with z.infer<>, so a malformed or unexpected shape from the optionGroupsQuery response is not validated at runtime. Optional chaining only guards against undefined, 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 use any."

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between ece87d6 and 7f586f9.

📒 Files selected for processing (31)
  • apps/playground/src/components/programs/forms/ProgramEventForm.tsx
  • apps/playground/src/components/programs/forms/ProgramRegistrationForm.tsx
  • apps/playground/src/components/programs/forms/ProgramRegistrationFormScreen.tsx
  • apps/playground/src/components/programs/forms/ProgramStageFormScreen.tsx
  • apps/playground/src/components/programs/forms/TrackerProgramShell.tsx
  • apps/playground/src/hooks/useOptionGroupsSupplementaryData.ts
  • apps/playground/src/pages/ProgramPage.test.tsx
  • apps/playground/src/pages/ProgramPage.tsx
  • components/dhis2-ui/src/fields/widgets/ChoiceFields.tsx
  • components/mantine/src/fields/widgets/ChoiceFields.tsx
  • components/mui/src/fields/widgets/ChoiceFields.tsx
  • docs/ARCHITECTURE.md
  • docs/form-state-architecture.md
  • docs/use-field-control-plan.md
  • docs/use-tracker-form.md
  • packages/metadata/src/index.ts
  • packages/metadata/src/optionGroups.test.ts
  • packages/metadata/src/optionGroups.ts
  • packages/metadata/src/queries/optionGroups.query.test.ts
  • packages/metadata/src/queries/optionGroups.query.ts
  • utils/hooks/src/fields/useFieldControl.test.ts
  • utils/hooks/src/fields/useFieldControl.ts
  • utils/hooks/src/formStore.ts
  • utils/hooks/src/test/renderFieldControl.tsx
  • utils/hooks/src/useEventForm.ts
  • utils/hooks/src/useTrackerForm.ts
  • utils/rules/src/filterPayload.test.ts
  • utils/rules/src/filterPayload.ts
  • utils/rules/src/index.ts
  • utils/rules/src/resolveHiddenOptionCodes.test.ts
  • utils/rules/src/resolveHiddenOptionCodes.ts

Comment on lines +36 to +42
useEffect(() => {
if (optionGroupIds.length > 0) {
refetch({ optionGroupIds })
}
}, [optionGroupIds, refetch])

return useMemo(() => (data ? resolveOptionGroups(data) : undefined), [data])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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/src

Repository: 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 -n

Repository: 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.

Comment thread docs/ARCHITECTURE.md
Comment on lines +143 to +153
`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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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 -220

Repository: 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-L456
  • docs/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.

Comment on lines +15 to +19
params: (variables: QueryVariables) => ({
fields: OPTION_GROUP_FIELDS,
filter: `id:in:[${(variables.optionGroupIds as string[]).join(',')}]`,
paging: false,
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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=ts

Repository: 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' . || true

Repository: 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.

Comment on lines +36 to +39
/** Sets option-group membership directly, without going through `init`/`reinit`. */
setOptionGroups(optionGroups: OptionGroupCodeMap | undefined): void {
this.optionGroupsRef = { current: optionGroups };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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/src

Repository: 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-L116
  • utils/hooks/src/useTrackerForm.ts#L66-L82
  • utils/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

Comment on lines +23 to +27
} else if (
typeof value === 'string' &&
resolveHiddenOptionCodes(state, optionGroups).has(value)
) {
result[key] = null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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=ts

Repository: 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' || true

Repository: 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)));
}
JS

Repository: 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:


🏁 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:


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.

@nnkogift
nnkogift merged commit 1a4ca7a into main Aug 3, 2026
13 of 14 checks passed
@nnkogift
nnkogift deleted the feat/hideoption-hideoptiongroup-support branch August 3, 2026 13:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant