Skip to content

feat: fine-tune field widgets (multi-select, boolean radios, calendars) - #23

Merged
cursor[bot] merged 4 commits into
mainfrom
cursor/fine-tune-field-widgets-df51
Aug 5, 2026
Merged

cursor[bot] merged 4 commits into
mainfrom
cursor/fine-tune-field-widgets-df51

Conversation

@nnkogift

@nnkogift nnkogift commented Aug 4, 2026 •

Copy link
Copy Markdown
Owner

Summary

Fine-tunes field widgets across hooks and all three UI adapters:

  • MULTI_TEXT + optionSet → multiSelect widget with comma-separated option codes
  • BOOLEAN → Yes/No radios (optional fields keep a clear — option)
  • DATE / AGE / TIME / DATETIME → real calendar/time pickers (datetime no longer stubbed)
  • Storybook fixtures, stories, and interaction tests updated accordingly
  • Rebased/merged with main (PRT Storybook fixtures) and updated program-rules boolean plays for radios

Test plan

  • Unit tests for widget kind, validation, and filterPayload multi-code filtering
  • Storybook browser tests (pre-merge)
  • fallow audit new-only gate passes
  • Spot-check Storybook stories for MultiSelect, Boolean, Date, Time, DateTime across adapters
  • CI green after merge with main
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features
    • Added multi-select fields with option handling across DHIS2, Mantine, and MUI interfaces.
    • Added date, time, date-time, and age field controls across supported UI libraries.
    • Boolean fields now use accessible radio-button controls.
  • Bug Fixes
    • Improved validation for multi-select values and date-time formats.
    • Hidden multi-select options are now correctly removed from submitted values.
  • Documentation
    • Updated field-control documentation for radio buttons and multi-select behavior.
  • Tests
    • Expanded coverage for new field types, validation, and value handling.

…alendars

Map MULTI_TEXT+optionSet to multiSelect with comma-separated codes, render
BOOLEAN as Yes/No radios, and replace native date/time inputs with calendar
pickers across dhis2-ui, mantine, and mui (including DATETIME).
@coderabbitai

coderabbitai Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cursor[bot], you've reached your PR review limit, so we couldn't start this review.

Next review available in: 49 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e8a5de25-8a1a-47cb-9e55-6e018dee72fc

📥 Commits

Reviewing files that changed from the base of the PR and between 8bbd710 and 1c0cbab.

📒 Files selected for processing (2)
  • apps/storybook/interactions/eventProgramRulesInteractions.ts
  • apps/storybook/interactions/trackerProgramRulesInteractions.ts
📝 Walkthrough

Walkthrough

The PR adds multiSelect, radio-based boolean fields, date/time/datetime widgets, shared value utilities, stricter validation, hidden-option filtering, and Storybook coverage for DHIS2, Mantine, and MUI adapters.

Changes

Field contracts and validation

Layer / File(s) Summary
Widget mapping and value validation
utils/hooks/src/fields/*, packages/metadata/src/*, apps/storybook/fixtures/fieldMetadata.ts, docs/use-field-control-plan.md
MULTI_TEXT option sets now resolve to multiSelect. CSV parsing and joining utilities are public. Multi-select values require known option codes. Datetime values use YYYY-MM-DDTHH:mm validation.

Adapter implementations

Layer / File(s) Summary
DHIS2 field controls
components/dhis2-ui/src/fields/*
Widget dispatch uses a component map. Boolean fields use radios. Multi-select, date, time, and datetime widgets are supported.
Mantine field controls
components/mantine/src/fields/*, components/mantine/package.json
Mantine radio, multi-select, date, time, datetime, and age controls are added. Date dependencies and exports are updated.
MUI field controls
components/mui/src/fields/*, components/mui/package.json
MUI radio, multi-select, localized date, time, datetime, and age controls are added. Date picker dependencies and exports are updated.

Shared behavior and Storybook coverage

Layer / File(s) Summary
Shared age and payload handling
utils/hooks/src/fields/computeAgeFromDob.*, utils/rules/src/filterPayload.*
Age calculation is shared. Hidden option codes are removed from multi-value payloads, and empty results become null.
Storybook stories and interactions
apps/storybook/interactions/*, apps/storybook/stories/*, apps/storybook/fixtures/*, apps/storybook/package.json, apps/storybook/.storybook/preview.tsx
Stories and play functions cover multi-select, time, datetime, and radio controls across adapters. Storybook date dependencies and styles are configured.

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

Sequence Diagram(s)

sequenceDiagram
  participant Storybook
  participant D2Field
  participant Widget
  participant FieldValidation
  Storybook->>D2Field: render widget kind
  D2Field->>Widget: select mapped field component
  Widget->>FieldValidation: validate changed value
  FieldValidation-->>Widget: accept or reject value
  Widget-->>Storybook: update field state
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.14% 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 accurately summarizes the main widget changes: multi-select support, boolean radio controls, and calendar-based date fields.
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 cursor/fine-tune-field-widgets-df51

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.

Strip invalid fallow-ignore reasons, suppress mirrored D2Field
duplication at file scope, and simplify temporal input queries so
new-only audit passes.
@nnkogift
nnkogift marked this pull request as ready for review August 5, 2026 04:51
cursor[bot]
cursor Bot approved these changes Aug 5, 2026 •

@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: 9

🧹 Nitpick comments (1)
utils/hooks/src/fields/computeAgeFromDob.test.ts (1)

5-13: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert exact ages with a fixed clock.

The current test only proves that the result is a positive numeric string. It passes when the age is off by one. Freeze the clock and assert exact values before, on, and after the birthday. Add an invalid calendar-date case such as 2024-02-31.

Proposed test improvement
-import { describe, expect, it } from 'vitest';
+import { afterEach, describe, expect, it, vi } from 'vitest';

 import { computeAgeFromDob } from './computeAgeFromDob';

+afterEach(() => vi.useRealTimers());
+
 describe('computeAgeFromDob', () => {
...
     it('computes whole years from a date of birth', () => {
-        expect(computeAgeFromDob('2000-01-01')).toMatch(/^\d+$/);
-        expect(Number(computeAgeFromDob('2000-01-01'))).toBeGreaterThan(0);
+        vi.useFakeTimers();
+        vi.setSystemTime(new Date(2025, 5, 15, 12));
+
+        expect(computeAgeFromDob('2000-01-01')).toBe('25');
+        expect(computeAgeFromDob('2000-06-15')).toBe('25');
+        expect(computeAgeFromDob('2000-06-16')).toBe('24');
+        expect(computeAgeFromDob('2024-02-31')).toBe('');
     });

Verify that the repository’s Vitest setup supports this clock-control pattern.

🤖 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/fields/computeAgeFromDob.test.ts` around lines 5 - 13,
Strengthen the tests for computeAgeFromDob by using the repository-supported
Vitest clock controls to freeze the current date, then assert exact ages for
dates before, on, and after the birthday. Extend the invalid-date cases with a
value such as 2024-02-31 and preserve the existing empty-string behavior.
🤖 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/storybook/interactions/fieldStoryInteractions.ts`:
- Around line 247-273: Update the dateInput and datetimeInput play functions to
exercise the adapter-specific picker controls rather than skipping read-only
inputs or only checking control presence. Select representative date and
datetime values through those picker interactions, then assert the fields’
serialized values; keep timeInput unchanged and use the existing field-query and
assertion helpers where applicable.

In `@components/dhis2-ui/src/fields/widgets/DateFields.tsx`:
- Around line 81-87: Update the commit function so it clears the field whenever
nextDate is empty, regardless of whether nextTime has a value, preventing
persistence of time-only values such as “T14:30”. Preserve the existing
empty-date-and-time handling and datetime construction for valid dates;
optionally disable the time input until a date is selected.
- Line 5: Update the todayIso helper used by D2DateField and D2DateTimeField to
derive the date from local calendar components instead of Date.toISOString(),
preserving the YYYY-MM-DD format used by the future-date limit.

In `@components/mantine/package.json`:
- Around line 28-35: Update the peerDependencies objects in
components/mantine/package.json (lines 28-35) and components/mui/package.json
(lines 28-36) to declare `@dhis2/app-runtime` and `@dhis2/rule-engine` as peer
dependencies, ensuring both adapter packages require them without bundling them.

In `@components/mui/package.json`:
- Around line 31-33: Align the `@mui/material` and `@mui/x-date-pickers` dependency
ranges with the selected `@mui/x-date-pickers` 9.10.1 peer-dependency contract,
avoiding ranges that permit incompatible future Material UI versions. Apply the
matching versions in components/mui/package.json lines 31-33 and 42-43, and
apps/storybook/package.json lines 25-27, keeping the MUI package pair consistent
across both packages.

In `@docs/use-field-control-plan.md`:
- Around line 831-851: Update the earlier WidgetKind example and the test-plan
example to reflect the resolved option-set mapping: option sets map to select
except MULTI_TEXT with an optionSet, which maps to multiSelect. Preserve the
existing select behavior for all other option-set value types and align both
examples with the current resolver and mapping table.

In `@packages/metadata/src/buildTeaFieldSchema.ts`:
- Line 13: Update the DATETIME validation anchored by DATETIME_PATTERN in
packages/metadata/src/buildTeaFieldSchema.ts:13-13 and the event-field schema in
utils/hooks/src/fields/fieldValidation.ts:5-5 to retain the existing format
check and add the same calendar- and clock-aware refinement, rejecting invalid
dates and times such as 2026-99-99T99:99 in both schemas.
- Around line 15-18: Update rejectFutureDates so DATETIME values are compared
using only their YYYY-MM-DD portion against todayIso(), preventing valid times
on the current date from being rejected while preserving the existing
future-date validation.

In `@utils/hooks/src/fields/computeAgeFromDob.ts`:
- Around line 3-10: Update the DOB parsing in the age-calculation function to
extract year, month, and day components, construct the birth date as a local
calendar date, and reject it when the constructed date’s components differ from
the parsed values, covering invalid dates such as February 31. Preserve the
existing invalid-input return and age calculation behavior while removing
reliance on new Date(dob).

---

Nitpick comments:
In `@utils/hooks/src/fields/computeAgeFromDob.test.ts`:
- Around line 5-13: Strengthen the tests for computeAgeFromDob by using the
repository-supported Vitest clock controls to freeze the current date, then
assert exact ages for dates before, on, and after the birthday. Extend the
invalid-date cases with a value such as 2024-02-31 and preserve the existing
empty-string behavior.
🪄 Autofix

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: 9f5bdeea-d3de-4ed1-bf75-fa0f0c797424

📥 Commits

Reviewing files that changed from the base of the PR and between c7088c9 and 4e8542c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (41)
  • apps/storybook/.storybook/preview.tsx
  • apps/storybook/fixtures/fieldMetadata.ts
  • apps/storybook/interactions/ancInteractions.ts
  • apps/storybook/interactions/fieldStoryInteractions.ts
  • apps/storybook/package.json
  • apps/storybook/stories/dhis2-ui/D2Field.stories.tsx
  • apps/storybook/stories/mantine/D2Field.stories.tsx
  • apps/storybook/stories/mui/D2Field.stories.tsx
  • components/dhis2-ui/src/fields/D2Field.tsx
  • components/dhis2-ui/src/fields/widgets/ChoiceFields.tsx
  • components/dhis2-ui/src/fields/widgets/DateFields.tsx
  • components/dhis2-ui/src/fields/widgets/NumericFields.tsx
  • components/dhis2-ui/src/fields/widgets/index.ts
  • components/mantine/package.json
  • components/mantine/src/fields/D2Field.tsx
  • components/mantine/src/fields/widgets/ChoiceFields.tsx
  • components/mantine/src/fields/widgets/DateFields.tsx
  • components/mantine/src/fields/widgets/TextField.tsx
  • components/mantine/src/fields/widgets/index.ts
  • components/mui/package.json
  • components/mui/src/fields/D2Field.tsx
  • components/mui/src/fields/widgets/ChoiceFields.tsx
  • components/mui/src/fields/widgets/DateFields.tsx
  • components/mui/src/fields/widgets/TextField.tsx
  • components/mui/src/fields/widgets/index.ts
  • docs/use-field-control-plan.md
  • packages/metadata/src/buildTeaFieldSchema.ts
  • packages/metadata/src/index.ts
  • packages/metadata/src/multiTextValue.test.ts
  • packages/metadata/src/multiTextValue.ts
  • utils/hooks/src/fields/computeAgeFromDob.test.ts
  • utils/hooks/src/fields/computeAgeFromDob.ts
  • utils/hooks/src/fields/fieldValidation.test.ts
  • utils/hooks/src/fields/fieldValidation.ts
  • utils/hooks/src/fields/multiTextValue.test.ts
  • utils/hooks/src/fields/multiTextValue.ts
  • utils/hooks/src/fields/widgetKind.test.ts
  • utils/hooks/src/fields/widgetKind.ts
  • utils/hooks/src/index.ts
  • utils/rules/src/filterPayload.test.ts
  • utils/rules/src/filterPayload.ts
💤 Files with no reviewable changes (2)
  • components/mantine/src/fields/widgets/TextField.tsx
  • components/mui/src/fields/widgets/TextField.tsx

Comment on lines 247 to 273
const dateInput: PlayFunction<FieldStoryArgs> = async ({ canvasElement }) => {
await typeIntoField(canvasElement, 'date', '2024-06-15');
const canvas = canvasOf(canvasElement);
const input = queryFieldInput(canvas, 'date');
await expect(input).toBeInTheDocument();
if (input instanceof HTMLInputElement && !input.readOnly) {
await userEvent.clear(input);
await userEvent.type(input, '2024-06-15');
await assertInputValue(input, '2024-06-15');
}
};

const timeInput: PlayFunction<FieldStoryArgs> = async ({ canvasElement }) => {
const canvas = canvasOf(canvasElement);
const input = queryFieldInput(canvas, 'time');
await expect(input).toBeInTheDocument();
if (input instanceof HTMLInputElement && !input.readOnly) {
await userEvent.clear(input);
await userEvent.type(input, '14:30');
await assertInputValue(input, '14:30');
}
};

const datetimeInput: PlayFunction<FieldStoryArgs> = async ({ canvasElement }) => {
const canvas = canvasOf(canvasElement);
const matches = canvas.queryAllByLabelText(labelPattern('datetime'));
await expect(matches.length).toBeGreaterThan(0);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Exercise temporal value changes in these plays.

dateInput does nothing when the adapter exposes a read-only picker input. datetimeInput only checks that a labeled control exists. These plays can pass while date or datetime selection does not update the field value.

Use adapter-specific picker interactions. Assert the resulting serialized date and datetime values after selection. As per coding guidelines, “Use Storybook in apps/storybook as the primary component documentation and integration/interaction test surface.”

🤖 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/storybook/interactions/fieldStoryInteractions.ts` around lines 247 -
273, Update the dateInput and datetimeInput play functions to exercise the
adapter-specific picker controls rather than skipping read-only inputs or only
checking control presence. Select representative date and datetime values
through those picker interactions, then assert the fields’ serialized values;
keep timeInput unchanged and use the existing field-query and assertion helpers
where applicable.

Source: Coding guidelines

import { resolveFieldValidation } from '@dhis2-form-utils/hooks';
import { computeAgeFromDob, resolveFieldValidation } from '@dhis2-form-utils/hooks';

const todayIso = (): string => new Date().toISOString().slice(0, 10);

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 | 🟡 Minor | ⚡ Quick win

Use the local calendar date for the future-date limit.

toISOString() converts the local time to UTC. It can allow tomorrow or reject today near a UTC day boundary. This affects both D2DateField and D2DateTimeField.

Proposed fix
-const todayIso = (): string => new Date().toISOString().slice(0, 10);
+const todayIso = (): string => {
+    const now = new Date();
+    const month = String(now.getMonth() + 1).padStart(2, '0');
+    const day = String(now.getDate()).padStart(2, '0');
+    return `${now.getFullYear()}-${month}-${day}`;
+};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const todayIso = (): string => new Date().toISOString().slice(0, 10);
const todayIso = (): string => {
const now = new Date();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
return `${now.getFullYear()}-${month}-${day}`;
};
🤖 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 `@components/dhis2-ui/src/fields/widgets/DateFields.tsx` at line 5, Update the
todayIso helper used by D2DateField and D2DateTimeField to derive the date from
local calendar components instead of Date.toISOString(), preserving the
YYYY-MM-DD format used by the future-date limit.

Comment on lines +81 to +87
const commit = (nextDate: string, nextTime: string) => {
if (!nextDate && !nextTime) {
field.onChange('');
return;
}
field.onChange(`${nextDate}T${nextTime || '00:00'}`);
};

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

Do not persist a time without a date.

If a user clears the date while a time exists, or enters time first, commit writes values such as T14:30. The next render passes that invalid date portion to CalendarInput, and strict datetime validation rejects the field. Clear the value when nextDate is empty. Optionally disable the time input until a date exists.

Proposed fix
-        if (!nextDate && !nextTime) {
+        if (!nextDate) {
             field.onChange('');
             return;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const commit = (nextDate: string, nextTime: string) => {
if (!nextDate && !nextTime) {
field.onChange('');
return;
}
field.onChange(`${nextDate}T${nextTime || '00:00'}`);
};
const commit = (nextDate: string, nextTime: string) => {
if (!nextDate) {
field.onChange('');
return;
}
field.onChange(`${nextDate}T${nextTime || '00:00'}`);
};
🤖 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 `@components/dhis2-ui/src/fields/widgets/DateFields.tsx` around lines 81 - 87,
Update the commit function so it clears the field whenever nextDate is empty,
regardless of whether nextTime has a value, preventing persistence of time-only
values such as “T14:30”. Preserve the existing empty-date-and-time handling and
datetime construction for valid dates; optionally disable the time input until a
date is selected.

Comment on lines 28 to 35
"peerDependencies": {
"@mantine/core": ">=7.0.0",
"@mantine/dates": ">=7.0.0",
"@mantine/hooks": ">=7.0.0",
"dayjs": ">=1.11.0",
"react": ">=18.0.0",
"react-dom": ">=18.0.0",
"react-hook-form": ">=7.0.0"

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

Declare the required DHIS2 peer dependencies in both adapter packages.

  • components/mantine/package.json#L28-L35: add @dhis2/app-runtime and @dhis2/rule-engine to peerDependencies.
  • components/mui/package.json#L28-L36: add @dhis2/app-runtime and @dhis2/rule-engine to peerDependencies.

As per coding guidelines, "**/package.json: Declare @dhis2/app-runtime and @dhis2/rule-engine as peer dependencies; they must not be bundled."

📍 Affects 2 files
  • components/mantine/package.json#L28-L35 (this comment)
  • components/mui/package.json#L28-L36
🤖 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 `@components/mantine/package.json` around lines 28 - 35, Update the
peerDependencies objects in components/mantine/package.json (lines 28-35) and
components/mui/package.json (lines 28-36) to declare `@dhis2/app-runtime` and
`@dhis2/rule-engine` as peer dependencies, ensuring both adapter packages require
them without bundling them.

Source: Coding guidelines

Comment on lines 31 to +33
"@mui/material": ">=6.0.0",
"@mui/x-date-pickers": ">=7.0.0",
"dayjs": ">=1.11.0",

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 | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package.json files =="
git ls-files '*package.json' | sed -n '1,120p'

echo
echo "== relevant package lines =="
for f in components/mui/package.json apps/storybook/package.json; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    nl -ba "$f" | sed -n '1,90p'
  else
    echo "missing $f"
  fi
done

echo
echo "== lockfiles / manifests mentioning package versions =="
for f in pnpm-lock.yaml apps/playground/package.json components/mui/package.json apps/storybook/package.json; do
  if [ -f "$f" ]; then
    echo "--- $f relevant ---"
    rg -n '`@mui/material`|`@mui/x-date-pickers`|dayjs' "$f" || true
  fi
done

Repository: nnkogift/dhis2-form-utils

Length of output: 635


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import json, pathlib
for rel in ["components/mui/package.json", "apps/storybook/package.json"]:
    p=pathlib.Path(rel)
    print(f"--- {rel} ---")
    if not p.exists():
        print("missing")
        continue
    data=json.loads(p.read_text())
    for sec in ["peerDependencies","devDependencies","dependencies"]:
        deps=data.get(sec,{})
        hits={k:v for k,v in deps.items() if k.startswith("`@mui/`") or k=="dayjs"}
        if hits:
            print(sec, hits)
    print("workspace:", data.get("workspace"))
PY

Repository: nnkogift/dhis2-form-utils

Length of output: 563


🌐 Web query:

@mui/x-date-pickers 9.10.1 supported @mui/material versions

💡 Result:

The @mui/x-date-pickers version 9.10.1 has the following peer dependency requirement for @mui/material: ^5.15.14 || ^6.0.0 || ^7.0.0 [1]. Although older versions of the peer dependency range may be technically listed in some manifests [1], MUI X v9 migration documentation explicitly requires upgrading to at least @mui/material version 7.3.0 or 9.0.0 (depending on the specific MUI ecosystem versioning at the time) to ensure compatibility and leverage required API changes, such as theming optimizations [2][3]. Compatibility with Material UI v5 and v6 is generally deprecated or requires specific configurations in MUI X v9, and users are strongly encouraged to upgrade to Material UI v7 or higher [2][4][3].

Citations:


Align the MUI X and Material UI version pair.

@mui/x-date-pickers 9.10.1 declares peerDependencies as ^5.15.14 || ^6.0.0 || ^7.0.0; @mui/material and the Storybook app should use a Material UI version compatible with the selected @mui/x-date-pickers version.

📍 Affects 2 files
  • components/mui/package.json#L31-L33 (this comment)
  • components/mui/package.json#L42-L43
  • apps/storybook/package.json#L25-L27
🤖 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 `@components/mui/package.json` around lines 31 - 33, Align the `@mui/material`
and `@mui/x-date-pickers` dependency ranges with the selected `@mui/x-date-pickers`
9.10.1 peer-dependency contract, avoiding ranges that permit incompatible future
Material UI versions. Apply the matching versions in components/mui/package.json
lines 31-33 and 42-43, and apps/storybook/package.json lines 25-27, keeping the
MUI package pair consistent across both packages.

Comment on lines +831 to +851
| `valueType` | `widgetKind` | DHIS2 UI | Mantine | Material UI |
| ----------------------------------- | ------------- | ----------------------------------- | -------------------------------- | --------------------------- |
| `TEXT`, `LETTER`, `URL`, `USERNAME` | `text` | `InputField` | `TextInput` | `TextField` |
| `LONG_TEXT` | `longText` | `TextAreaField` | `Textarea` | `TextField multiline` |
| `EMAIL` | `email` | `InputField type=email` | `TextInput type=email` | `TextField type=email` |
| `PHONE_NUMBER` | `phone` | `InputField type=tel` | `TextInput type=tel` | `TextField type=tel` |
| `NUMBER`, `UNIT_INTERVAL` | `number` | `InputField type=number` | `NumberInput` | `TextField type=number` |
| `INTEGER`, `INTEGER_*` | `integer` | `InputField type=number step=1` | `NumberInput allowDecimal=false` | `TextField type=number` |
| `PERCENTAGE` | `percentage` | `InputField` + `%` suffix | `NumberInput` + `%` | `TextField` + `%` adornment |
| `BOOLEAN` | `boolean` | Yes/No/(—) `Radio` | Yes/No/(—) `Radio.Group` | Yes/No/(—) `RadioGroup` |
| `TRUE_ONLY` | `trueOnly` | `Checkbox` | `Checkbox` | `Checkbox` |
| `DATE` | `date` | `CalendarInput` | `DateInput` | `DatePicker` |
| `DATETIME` | `datetime` | `CalendarInput` + `InputField` time | `DateTimePicker` | `DateTimePicker` |
| `TIME` | `time` | `InputField type=time` | `TimeInput` | `TimePicker` |
| `AGE` | `age` | `CalendarInput` + age display | `DateInput` + age display | `DatePicker` + age display |
| any + `optionSet` (not MULTI_TEXT) | `select` | `SingleSelectField` | `Select` | `Select` |
| `MULTI_TEXT` + `optionSet` | `multiSelect` | `MultiSelectField` (CSV codes) | `MultiSelect` (CSV codes) | multi `TextField select` |
| `COORDINATE` | `coordinate` | Two `InputField` (lat + lng) | Two `NumberInput` | Two `TextField` |
| `FILE_RESOURCE` | `file` | `FileInputField` | `FileInput` | `Button` file type |
| `IMAGE` | `image` | `FileInputField` + preview | `FileInput` + preview | `Button` file + preview |
| `ORGANISATION_UNIT` | `orgUnit` | DHIS2 `OrgUnitField` | Custom tree picker | Custom tree picker |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Synchronize the remaining plan examples with the resolved mapping.

The new table and the resolved multiSelect note are correct. The earlier WidgetKind example at Lines 316-340 and the test-plan example at Lines 929-932 still omit multiSelect and state that every option set maps to select. Update those examples to include the MULTI_TEXT branch.

Proposed documentation update
-    | 'select' // any valueType WITH an optionSet (single-select)
+    | 'select' // any valueType WITH an optionSet except MULTI_TEXT
+    | 'multiSelect' // MULTI_TEXT WITH an optionSet
...
-    if (config.optionSet) return 'select';
+    if (config.optionSet) {
+        return config.valueType === 'MULTI_TEXT' ? 'multiSelect' : 'select';
+    }
...
-        case 'LONG_TEXT':
+        case 'LONG_TEXT':
+        case 'MULTI_TEXT':

This follows the current resolver implementation and the updated plan table.

Also applies to: 1009-1015

🤖 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/use-field-control-plan.md` around lines 831 - 851, Update the earlier
WidgetKind example and the test-plan example to reflect the resolved option-set
mapping: option sets map to select except MULTI_TEXT with an optionSet, which
maps to multiSelect. Preserve the existing select behavior for all other
option-set value types and align both examples with the current resolver and
mapping table.

};

const todayIso = (): string => new Date().toISOString().slice(0, 10);
const DATETIME_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/;

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

Validate calendar and clock ranges in both DATETIME schemas.

Both regular expressions check only digit counts. They accept invalid values such as 2026-99-99T99:99, so invalid form values can pass validation.

  • packages/metadata/src/buildTeaFieldSchema.ts#L13-L13: add a calendar-aware refinement after the format check.
  • utils/hooks/src/fields/fieldValidation.ts#L5-L5: apply the same refinement to the event-field schema.
📍 Affects 2 files
  • packages/metadata/src/buildTeaFieldSchema.ts#L13-L13 (this comment)
  • utils/hooks/src/fields/fieldValidation.ts#L5-L5
🤖 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/buildTeaFieldSchema.ts` at line 13, Update the DATETIME
validation anchored by DATETIME_PATTERN in
packages/metadata/src/buildTeaFieldSchema.ts:13-13 and the event-field schema in
utils/hooks/src/fields/fieldValidation.ts:5-5 to retain the existing format
check and add the same calendar- and clock-aware refinement, rejecting invalid
dates and times such as 2026-99-99T99:99 in both schemas.

Comment on lines 15 to 18
const rejectFutureDates = (schema: z.ZodTypeAny): z.ZodTypeAny =>
schema.refine((value) => typeof value !== 'string' || value === '' || value <= todayIso(), {
message: 'Date cannot be in the future',
});

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

Compare the date portion for DATETIME future checks.

rejectFutureDates compares a YYYY-MM-DDTHH:mm value with date-only todayIso(). On August 5, 2026, 2026-08-05T09:00 is lexically greater than 2026-08-05, so allowFutureDate=false rejects every datetime on August 5, 2026. Compare value.slice(0, 10) with todayIso(), or compare full timestamps.

Minimal fix
-        value <= todayIso(),
+        value.slice(0, 10) <= todayIso(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const rejectFutureDates = (schema: z.ZodTypeAny): z.ZodTypeAny =>
schema.refine((value) => typeof value !== 'string' || value === '' || value <= todayIso(), {
message: 'Date cannot be in the future',
});
const rejectFutureDates = (schema: z.ZodTypeAny): z.ZodTypeAny =>
schema.refine((value) => typeof value !== 'string' || value === '' || value.slice(0, 10) <= todayIso(), {
message: 'Date cannot be in the future',
});
🤖 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/buildTeaFieldSchema.ts` around lines 15 - 18, Update
rejectFutureDates so DATETIME values are compared using only their YYYY-MM-DD
portion against todayIso(), preventing valid times on the current date from
being rejected while preserving the existing future-date validation.

Comment on lines +3 to +10
if (!/^\d{4}-\d{2}-\d{2}$/.test(dob)) return '';
const birth = new Date(dob);
if (Number.isNaN(birth.getTime())) return '';
const today = new Date();
let age = today.getFullYear() - birth.getFullYear();
const monthDiff = today.getMonth() - birth.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
age -= 1;

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/sh
set -eu
TZ=America/Los_Angeles node - <<'NODE'
const parsed = new Date('2000-01-01');
console.log(parsed.getFullYear(), parsed.getMonth() + 1, parsed.getDate());
console.log(new Date('2024-02-31').toString());
NODE

Repository: nnkogift/dhis2-form-utils

Length of output: 232


🏁 Script executed:

#!/bin/bash
set -eu

printf 'Repo files matching computeAgeFromDob:\n'
fd -a 'computeAgeFromDob\.ts$|field.*dob|Date' . | sed 's#^\./##' | head -100

printf '\nTarget file:\n'
cat -n utils/hooks/src/fields/computeAgeFromDob.ts

printf '\nUsages of computeAgeFromDob:\n'
rg -n "computeAgeFromDob|ageFromDob|Age.*DOB|Date.*DOB" utils apps . --glob '!node_modules' --glob '!dist' --glob '!build' | head -120

printf '\nLocate date widget/value adapters in hooks:\n'
fd -t f '.*' utils/hooks/src | rg -i "date|dob|field" | head -150

Repository: nnkogift/dhis2-form-utils

Length of output: 4607


Parse date-only DOB values as local calendar dates.

new Date(dob) parses an ISO date-only string at UTC midnight, but getFullYear(), getMonth(), and getDate() read local time. With negative UTC offsets, 2000-01-01 can become December 31, 1999 in the local calendar and advance the age one day early.

This parser also normalizes invalid calendar dates like 2024-02-31 to 2024-03-01; the existing checks still accept this. Parse the date components, construct a local date, reject any normalized component, then calculate the age.

Proposed fix
-    if (!/^\d{4}-\d{2}-\d{2}$/.test(dob)) return '';
-    const birth = new Date(dob);
-    if (Number.isNaN(birth.getTime())) return '';
+    const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dob);
+    if (!match) return '';
+    const year = Number(match[1]);
+    const month = Number(match[2]);
+    const day = Number(match[3]);
+    const birth = new Date(year, month - 1, day);
+    if (
+        birth.getFullYear() !== year ||
+        birth.getMonth() !== month - 1 ||
+        birth.getDate() !== day
+    ) {
+        return '';
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!/^\d{4}-\d{2}-\d{2}$/.test(dob)) return '';
const birth = new Date(dob);
if (Number.isNaN(birth.getTime())) return '';
const today = new Date();
let age = today.getFullYear() - birth.getFullYear();
const monthDiff = today.getMonth() - birth.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
age -= 1;
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dob);
if (!match) return '';
const year = Number(match[1]);
const month = Number(match[2]);
const day = Number(match[3]);
const birth = new Date(year, month - 1, day);
if (
birth.getFullYear() !== year ||
birth.getMonth() !== month - 1 ||
birth.getDate() !== day
) {
return '';
}
const today = new Date();
let age = today.getFullYear() - birth.getFullYear();
const monthDiff = today.getMonth() - birth.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
age -= 1;
🤖 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/fields/computeAgeFromDob.ts` around lines 3 - 10, Update the
DOB parsing in the age-calculation function to extract year, month, and day
components, construct the birth date as a local calendar date, and reject it
when the constructed date’s components differ from the parsed values, covering
invalid dates such as February 31. Preserve the existing invalid-input return
and age calculation behavior while removing reliance on new Date(dob).

Resolve ChoiceFields conflict keeping Radio.Group boolean widgets,
drop obsolete ancInteractions, and update event/tracker program-rules
plays to click Yes/No radios across adapters.

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

Stale comment

Left a non-blocking comment (not approving): Cursor Security Agent was present but completed as skipped/failed to start, so required security review did not finish successfully. Cursor Bugbot was not present after the initial poll. Human review is needed before merge; no eligible non-author reviewers were available to assign.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

Fieldset textContent includes Yes/No options, so anchored label
regexes failed; match the legend instead.

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

Left a non-blocking comment (not approving): Cursor Security Agent was present but completed as skipped, so required security review did not finish successfully. Cursor Bugbot was not present after the initial poll. Human review is needed; no eligible non-author reviewers were available to assign.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@cursor
cursor Bot merged commit e009dd5 into main Aug 5, 2026
14 checks passed
@cursor
cursor Bot deleted the cursor/fine-tune-field-widgets-df51 branch August 5, 2026 05:29
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.

2 participants