Repository navigation
feat: redesign program rules playground with rule-effect visualization - #12
Conversation
Implements the design in apps/playground/design/Design handoff.md: program list restyle, a merged tracker/event program page (context bar + enrollment rail + unified rule panel), and a rule-effect visualization system that shows which program rule hid, required, assigned, warned, or errored on each field/section, including ghost placeholders for hidden slots. - New @dhis2-form-utils/hooks `useRuleEffectTrace`/`useFieldRuleEffect`/ `useSectionRuleEffect` generalize the trace-to-rule lookup previously internal to devtools, so any consumer can attribute a field/section effect back to the rule that caused it. - New playground `RuleAwareField`/`FormSectionCard` call `useFieldControl`/ `useSectionState` directly instead of `D2Field`/`FormSection`, so hidden fields/sections can render as ghost placeholders instead of `null`. - Merged `ProgramRulesPanel` + `RuleDevtoolsPanel` into a single `RulesPanel` (Rules/Trace/Graph tabs) in @dhis2-form-utils/devtools, with scope-aware 4-state rule cards (selected/firing/idle/out-of-scope). - New `EnrollmentRail` + `TrackerProgramShell` render every tracker stage and repeatable-stage event as an independently-mounted form (CSS-hidden rather than unmounted) so switching slots never discards in-progress data. - Program list: segmented type filter, rule/stage counts, whole-row navigation; org unit and date fields moved out of the form into a shared `ProgramContextBar`. - Excludes apps/playground/design/** (reference/design-tool material, including a generated design-system export bundle) from eslint, prettier, and git. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 53 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThe change adds the Program Rules Playground, replaces the program placeholder route, introduces tracker navigation and rule-aware forms, consolidates rule devtools into ChangesProgram Rules Playground
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant ProgramPage
participant TrackerProgramShell
participant EnrollmentRail
participant ProgramStageFormScreen
participant RulesPanel
ProgramPage->>TrackerProgramShell: load tracker metadata and selected context
TrackerProgramShell->>EnrollmentRail: render registration and stage slots
EnrollmentRail-->>TrackerProgramShell: select stage or create event draft
TrackerProgramShell->>ProgramStageFormScreen: render selected stage form
ProgramStageFormScreen->>RulesPanel: render rule catalog, trace, and graph
RulesPanel-->>ProgramStageFormScreen: display rule effects and selections
🚥 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: 15
🧹 Nitpick comments (10)
apps/playground/design/Program Rules Playground.dc.html (2)
1037-1037: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRemove the no-op filter.
(s) => (ghosts ? true : s)keeps every section in both branches, because each section object is truthy. Drop the.filter(...)call.♻️ Proposed fix
- ].filter((s) => (ghosts ? true : s)); + ];🤖 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/design/Program` Rules Playground.dc.html at line 1037, Remove the no-op .filter call following the sections array construction, preserving the array contents and surrounding ghosts logic unchanged.
1060-1060: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSet
badgeTitleso the badge tooltip names the rule.Line 1060 and Line 1242 omit
badgeTitle, sotitlerenders as an empty string. The handoff specifies the tooltip text<ACTIONTYPE> from rule: <rule name>, and the sibling variants at Lines 1030-1031 already follow it.♻️ Proposed fix
- : field('Referral to clinic', '', { badge: 'hide', badgeText: 'Hidden' }), + : field('Referral to clinic', '', { badge: 'hide', badgeText: 'Hidden', badgeTitle: 'HIDEFIELD from rule: Hide referral unless underweight' }),- field('Date treatment started', '', { badge: 'warning', badgeText: 'Warning', ring: '`#ffa902`', help: 'Treatment recorded without a start date.', helpFg: '`#6f3205`' }), + field('Date treatment started', '', { badge: 'warning', badgeText: 'Warning', badgeTitle: 'SHOWWARNING from rule: Warn on missing treatment date', ring: '`#ffa902`', help: 'Treatment recorded without a start date.', helpFg: '`#6f3205`' }),Also applies to: 1242-1242
🤖 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/design/Program` Rules Playground.dc.html at line 1060, Update the Referral to clinic rule definitions at the shown locations to include badgeTitle using the specified “<ACTIONTYPE> from rule: <rule name>” tooltip format, matching the sibling variants’ implementation and preserving the existing hidden badge configuration.apps/playground/design/Design handoff.md (1)
44-49: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAlign the planned file list with the implemented components.
The plan lists
components/rules/RulePanel.tsxandcomponents/rules/RuleCatalogList.tsxinside the playground app. The PR delivers the unified panel asutils/devtools/src/RulesPanel.tsx, and deliversRuleFeedbackList.tsx,FieldEffectBadge.tsx,HiddenFieldPlaceholder.tsx, andHiddenSectionPlaceholder.tsxunderapps/playground/src/components/rules/. Update the two panel entries so the handoff matches the shipped layout.🤖 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/design/Design` handoff.md around lines 44 - 49, Update the planned panel entries in the handoff file list: replace the playground-local RulePanel.tsx and RuleCatalogList.tsx paths with the shipped unified panel path utils/devtools/src/RulesPanel.tsx, while keeping the listed apps/playground/src/components/rules components aligned with their delivered locations.apps/playground/eslint.config.mjs (1)
22-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winNarrow the
import/nameddisable to the affected files.This override applies to every file matched by this config. The stated resolver limitation concerns the dynamic
@dhis2/ui-iconsre-export. A config-wide override can hide unrelated invalid named imports. Scope the override to affected files or fix the resolver configuration. Keep TypeScript typechecking as the backstop.🤖 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/eslint.config.mjs` around lines 22 - 25, Scope the 'import/named' override in the ESLint configuration to only the files importing the dynamically re-exported `@dhis2/ui-icons` symbols, rather than disabling it globally. Preserve TypeScript typechecking as the validation backstop and leave the rule enabled for unrelated files.apps/playground/src/utils/resolveProgramListItem.ts (1)
14-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd unit tests for
resolveProgramListItem.This is a new pure function with edge-case defaulting logic (missing
lastUpdated, missingprogramStages/programRules). No test file exists for it in this batch. Add unit tests covering: a fully-populated raw item, a raw item missinglastUpdated, and a raw item missingprogramStages/programRules.🤖 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/utils/resolveProgramListItem.ts` around lines 14 - 25, Add unit tests for the pure resolveProgramListItem function covering a fully populated raw item, a missing lastUpdated defaulting to an empty string, and missing programStages/programRules defaulting their counts to zero. Assert the complete mapped Program result for each scenario using the project’s existing test conventions.apps/playground/src/utils/formatLastUpdated.ts (1)
7-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd unit tests for
formatLastUpdated.The function has two early-return branches (empty string, invalid date) besides the normal formatting path. No test file exists for it. Add unit tests for a valid ISO date, an empty string, and an invalid date string.
🤖 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/utils/formatLastUpdated.ts` around lines 7 - 18, Add unit tests for formatLastUpdated covering a valid ISO date with the expected formatted output, an empty string returning '', and an invalid date string returning ''. Create the appropriate test file using the project’s existing testing conventions.utils/devtools/src/RulesPanel.tsx (2)
345-370: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winPrefer a real
buttonoverrole="button"on thearticle.The
articlecarriesrole="button", which overrides the article semantics and places anh3inside an interactive control. Several screen readers drop headings from the accessibility tree of a button, so the rule name loses its heading semantics. A nestedbuttonthat wraps the title, or abuttonelement in place of thearticle, keeps both the semantics and the keyboard handling that you now implement by hand.🤖 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/devtools/src/RulesPanel.tsx` around lines 345 - 370, Replace the interactive article container with a real button element, removing the role, tabIndex, and manual onKeyDown handling while preserving the existing onClick selection behavior and styling. Update the surrounding structure as needed so the rule name remains exposed as a heading rather than being nested inside a button; use the rule card markup around the visible h3 as the change point.
158-165: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winMemoize
graphProps.formValues.
form.getValues()changes each render, sographPropschanges and breaksRuleGraphViewmemoization.toFlowGraph()depends onformValues, recomputes the layout, updates ReactFlow nodes/edges, and also changesedgeCount/nodeCountinFitViewOnChange; memoize the form values and use a consistent key so the graph only recomputes when values, layout key, or graph data change.🤖 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/devtools/src/RulesPanel.tsx` around lines 158 - 165, Update the graphProps construction around form.getValues() to memoize formValues with a stable dependency/key, and use that same consistent key when memoizing graphProps so RuleGraphView only receives new props when form values, layout state, or graph data actually change. Preserve the existing entries, fieldState, selectedEntryId, highlightRuleId, and labelLookup behavior.apps/playground/src/components/rules/RuleFeedbackList.tsx (1)
57-58: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winThe
useMemonever caches, and the label lookup is built twice.Both callers pass a new object literal for
metadata. SeeProgramEventForm.tsxlines 124-130 andProgramRegistrationForm.tsx. The dependency therefore changes on every render, socreateLabelLookupruns on every render.The same callers already build an identical lookup and supply it through
RuleDisplayProvider. Consume that lookup withuseRuleDisplay()and drop themetadataprop. This removes the duplicate construction and the ineffective memo.♻️ Proposed refactor
-type RuleFeedbackListProps = { - metadata: RuleDevtoolsMetadata -} - -export function RuleFeedbackList({ metadata }: RuleFeedbackListProps) { - const labelLookup = useMemo(() => createLabelLookup(metadata), [metadata]) +export function RuleFeedbackList() { + const { labelLookup } = useRuleDisplay() const feedback = useFormFeedback()Callers then render
<RuleFeedbackList />inside the existingRuleDisplayProvider.🤖 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/components/rules/RuleFeedbackList.tsx` around lines 57 - 58, Update RuleFeedbackList to remove the metadata prop and the ineffective useMemo/createLabelLookup construction, and consume the existing label lookup via useRuleDisplay(). Update its callers to render RuleFeedbackList within the existing RuleDisplayProvider while preserving current display behavior.apps/playground/src/components/programs/forms/TrackerProgramShell.tsx (1)
60-72: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winMount stage slots only after their first visit.
Every non-repeatable stage renders immediately. Each slot mounts a
ProgramEventForm, which creates its own form store, rule evaluation loop, andRulesPanel. A program with many stages therefore pays the full cost for stages the user never opens.State preservation only requires that a slot stays mounted after the user opens it. Track the visited slot keys and filter
renderableStageSlotsby that set.♻️ Proposed refactor
+ const visitedKeys = useRef<Set<string>>(new Set()) + const renderableStageSlots = useMemo<RenderableStageSlot[]>(() => { const slots: RenderableStageSlot[] = [] for (const stage of program.programStages ?? []) { if (stage.repeatable) { for (const eventLocalId of eventDraftsByStage[stage.id] ?? []) { slots.push({ stageId: stage.id, eventLocalId }) } } else { slots.push({ stageId: stage.id, eventLocalId: 'primary' }) } } return slots }, [program.programStages, eventDraftsByStage])Then filter in the render body:
if (isSelected) { visitedKeys.current.add(key) } if (!visitedKeys.current.has(key)) { return null }🤖 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/components/programs/forms/TrackerProgramShell.tsx` around lines 60 - 72, Track visited stage-slot keys in TrackerProgramShell using a persistent ref, mark the selected slot as visited during rendering, and filter unvisited entries before mounting their ProgramEventForm. Preserve mounted slots after first visit so their state remains intact, while preventing non-selected stages from mounting initially; update the renderableStageSlots/render loop rather than changing slot generation semantics.
🤖 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/design/Program` Rules Playground App.dc.html:
- Around line 467-469: Update the residence field branch near the hidden
registration row so it returns a badge-only field when ghosts are disabled
instead of null. Mirror the existing badge-only behavior used by the
corresponding stageSections and eventSections branches, preserving the current
ghost rendering when g is enabled and keeping the hide badge and label visible
otherwise.
In `@apps/playground/src/components/programs/EnrollmentRail.tsx`:
- Around line 39-52: Add an aria-current attribute to the role="button" element
in EnrollmentRail, deriving its value from the selected state so assistive
technology identifies the currently selected row while preserving the existing
interaction and styling.
In `@apps/playground/src/components/programs/forms/ProgramRegistrationForm.tsx`:
- Around line 149-151: Update RuleFeedbackList and its usage in
ProgramRegistrationForm to consume the existing labelLookup from
RuleDisplayProvider context instead of receiving a newly created, incomplete
metadata object. Ensure stage names resolve through the lookup that includes
programStages, and remove the duplicate metadata-based lookup construction and
per-render object recreation.
In `@apps/playground/src/components/programs/ProgramContextBar.tsx`:
- Around line 59-74: Update the selected prop in the SingleSelectField within
ProgramContextBar to pass undefined when orgUnitId is empty, while preserving
valid org-unit IDs as selected values.
In `@apps/playground/src/components/rules/HiddenSectionPlaceholder.tsx`:
- Around line 18-21: Update the translation rendering in
HiddenSectionPlaceholder so the complete “Section hidden by” sentence and
ruleName are handled as one translation unit, using react-i18next’s Trans
component if needed to preserve the bold styling of ruleName; remove the
fragment translation and adjacent string concatenation.
In `@apps/playground/src/components/rules/RuleAwareField.tsx`:
- Around line 28-57: Update RuleAwareField to preserve the field’s accessible
name when rendering D2FieldWidget: assign or reuse a stable input id, connect
the wrapper label span to that id via the appropriate label association, and
pass the original label and id through widgetControl instead of blanking
fieldConfig.label. Ensure the association also works for D2TextField,
D2LongTextField, D2DateField, and other widgets that forward fieldConfig values
to form controls.
In `@apps/playground/src/components/rules/RuleFeedbackList.tsx`:
- Around line 40-43: Update the feedback row rendering around item.content and
item.value to branch on item.type: render text-only feedback without the
trailing colon or value, while preserving the existing “content: value” layout
for non-text items.
In `@apps/playground/src/pages/ProgramPage.tsx`:
- Around line 38-39: Update handleResetPlayground and the rendered
TrackerProgramShell/ProgramStageFormScreen flow to use a reset counter that
increments on each reset and is passed as the component key, forcing a full
remount. Preserve the existing orgUnitId and primaryDate clearing while ensuring
internal stage, event, draft, and form-hook state is discarded.
- Around line 68-82: Update the error-rendering branch in ProgramPage around
useEventProgramMetadataQuery so it distinguishes a missing program from other
query failures. Use the query’s exposed error details or status to render
“Program not found” only for a confirmed not-found response, and show an
appropriate generic error message for network or server failures while
preserving the existing navigation and error layout.
In `@components/dhis2-ui/src/fields/D2Field.tsx`:
- Around line 27-29: Replace the duplicate D2FieldWidgetProps declaration in
D2Field.tsx with the canonical WidgetProps type imported from
`@dhis2-form-utils/hooks`, and update any references or exports to use that shared
type while preserving the existing control prop contract.
In `@utils/devtools/src/index.ts`:
- Around line 1-20: Update residual references to the removed ProgramRulesPanel
and RuleDevtoolsPanel symbols in CLAUDE.md, docs/dev-tools.md, and the
playground design documentation. Replace them with the current RulesPanel and
RuleDevtoolsScope APIs where applicable, or clearly mark historical references
as replaced content; do not alter the exports in index.ts.
In `@utils/devtools/src/RulesPanel.tsx`:
- Around line 327-333: Update the empty-state message in the RulesPanel catalog
check to state that the program has no rules, removing the “current form
context” wording that implies filtering. Keep the existing translate call and
rendering structure unchanged.
- Around line 286-293: Update the conditional rendering around the screen-reader
summary in RulesPanel so it renders when entries exist rather than only when
entries.length is zero, allowing the evaluations and observedRuleCount values to
be announced. Preserve the tab !== 'rules' condition and add aria-live to the
summary paragraph so updated counts are announced.
- Around line 81-85: Move tab label translation out of the module-level
PANEL_TABS constant so labels resolve during RulesPanel rendering. Preserve the
tab keys in a static PANEL_TAB_KEYS collection, then map over those keys in the
tab list and call resolveTabLabel(key) for each tab, ensuring locale updates are
reflected without changing tab order or behavior.
In `@utils/hooks/src/useRuleEffectTrace.test.tsx`:
- Around line 74-79: Hoist the metadataWithRules() fixture to a stable
module-level reference and use that reference in useHarness’s useTrackerForm
options.metadata. Keep the existing form and formStore return behavior unchanged
so renders do not trigger repeated schema or rule-engine reinitialization.
---
Nitpick comments:
In `@apps/playground/design/Design` handoff.md:
- Around line 44-49: Update the planned panel entries in the handoff file list:
replace the playground-local RulePanel.tsx and RuleCatalogList.tsx paths with
the shipped unified panel path utils/devtools/src/RulesPanel.tsx, while keeping
the listed apps/playground/src/components/rules components aligned with their
delivered locations.
In `@apps/playground/design/Program` Rules Playground.dc.html:
- Line 1037: Remove the no-op .filter call following the sections array
construction, preserving the array contents and surrounding ghosts logic
unchanged.
- Line 1060: Update the Referral to clinic rule definitions at the shown
locations to include badgeTitle using the specified “<ACTIONTYPE> from rule:
<rule name>” tooltip format, matching the sibling variants’ implementation and
preserving the existing hidden badge configuration.
In `@apps/playground/eslint.config.mjs`:
- Around line 22-25: Scope the 'import/named' override in the ESLint
configuration to only the files importing the dynamically re-exported
`@dhis2/ui-icons` symbols, rather than disabling it globally. Preserve TypeScript
typechecking as the validation backstop and leave the rule enabled for unrelated
files.
In `@apps/playground/src/components/programs/forms/TrackerProgramShell.tsx`:
- Around line 60-72: Track visited stage-slot keys in TrackerProgramShell using
a persistent ref, mark the selected slot as visited during rendering, and filter
unvisited entries before mounting their ProgramEventForm. Preserve mounted slots
after first visit so their state remains intact, while preventing non-selected
stages from mounting initially; update the renderableStageSlots/render loop
rather than changing slot generation semantics.
In `@apps/playground/src/components/rules/RuleFeedbackList.tsx`:
- Around line 57-58: Update RuleFeedbackList to remove the metadata prop and the
ineffective useMemo/createLabelLookup construction, and consume the existing
label lookup via useRuleDisplay(). Update its callers to render RuleFeedbackList
within the existing RuleDisplayProvider while preserving current display
behavior.
In `@apps/playground/src/utils/formatLastUpdated.ts`:
- Around line 7-18: Add unit tests for formatLastUpdated covering a valid ISO
date with the expected formatted output, an empty string returning '', and an
invalid date string returning ''. Create the appropriate test file using the
project’s existing testing conventions.
In `@apps/playground/src/utils/resolveProgramListItem.ts`:
- Around line 14-25: Add unit tests for the pure resolveProgramListItem function
covering a fully populated raw item, a missing lastUpdated defaulting to an
empty string, and missing programStages/programRules defaulting their counts to
zero. Assert the complete mapped Program result for each scenario using the
project’s existing test conventions.
In `@utils/devtools/src/RulesPanel.tsx`:
- Around line 345-370: Replace the interactive article container with a real
button element, removing the role, tabIndex, and manual onKeyDown handling while
preserving the existing onClick selection behavior and styling. Update the
surrounding structure as needed so the rule name remains exposed as a heading
rather than being nested inside a button; use the rule card markup around the
visible h3 as the change point.
- Around line 158-165: Update the graphProps construction around
form.getValues() to memoize formValues with a stable dependency/key, and use
that same consistent key when memoizing graphProps so RuleGraphView only
receives new props when form values, layout state, or graph data actually
change. Preserve the existing entries, fieldState, selectedEntryId,
highlightRuleId, and labelLookup behavior.
🪄 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: 1f46f5b0-a5e4-47df-8f5b-2cfe3d49b027
📒 Files selected for processing (64)
.gitignore.prettierignoreapps/playground/design/Design handoff.mdapps/playground/design/Program Rules Playground App.dc.htmlapps/playground/design/Program Rules Playground.dc.htmlapps/playground/design/github.mdapps/playground/design/support.jsapps/playground/eslint.config.mjsapps/playground/i18n/en.potapps/playground/src/AppWrapper.tsxapps/playground/src/components/programs/EnrollmentRail.tsxapps/playground/src/components/programs/ProgramContextBar.tsxapps/playground/src/components/programs/ProgramListFilters.tsxapps/playground/src/components/programs/ProgramListTable.test.tsxapps/playground/src/components/programs/ProgramListTable.tsxapps/playground/src/components/programs/forms/EventFormFields.tsxapps/playground/src/components/programs/forms/EventSystemFields.tsxapps/playground/src/components/programs/forms/FormSectionCard.tsxapps/playground/src/components/programs/forms/OrganisationUnitSelectField.tsxapps/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/RegistrationFormFields.tsxapps/playground/src/components/programs/forms/TrackerProgramShell.tsxapps/playground/src/components/programs/forms/TrackerSystemFields.tsxapps/playground/src/components/programs/trackerSlot.tsapps/playground/src/components/rules/FieldEffectBadge.tsxapps/playground/src/components/rules/GhostToggleButton.tsxapps/playground/src/components/rules/HiddenFieldPlaceholder.tsxapps/playground/src/components/rules/HiddenSectionPlaceholder.tsxapps/playground/src/components/rules/RuleAwareField.tsxapps/playground/src/components/rules/RuleDisplayContext.tsxapps/playground/src/components/rules/RuleFeedbackList.tsxapps/playground/src/components/rules/ghostBackground.tsapps/playground/src/hooks/useProgramHeader.tsapps/playground/src/hooks/usePrograms.tsapps/playground/src/index.cssapps/playground/src/pages/ProgramListPage.tsxapps/playground/src/pages/ProgramPage.test.tsxapps/playground/src/pages/ProgramPage.tsxapps/playground/src/pages/ProgramPlaceholderPage.tsxapps/playground/src/types/program.tsapps/playground/src/utils/formatLastUpdated.tsapps/playground/src/utils/formatProgramType.tsapps/playground/src/utils/resolveProgramListItem.tscomponents/dhis2-ui/src/fields/D2Field.tsxcomponents/dhis2-ui/src/index.tseslint.config.jspackages/config/tailwind-theme.csspackages/metadata/src/fieldFilters.tspackages/metadata/src/queries/fields.const.test.tsutils/devtools/src/ProgramRulesPanel.tsxutils/devtools/src/RuleDevtoolsPanel.tsxutils/devtools/src/RulesPanel.tsxutils/devtools/src/buildGraph.test.tsutils/devtools/src/createLabelLookup.test.tsutils/devtools/src/createLabelLookup.tsutils/devtools/src/formatRuleActionSummary.test.tsutils/devtools/src/index.tsutils/devtools/src/resolveProgramRulesList.tsutils/hooks/src/index.tsutils/hooks/src/useRuleEffectTrace.test.tsxutils/hooks/src/useRuleEffectTrace.ts
💤 Files with no reviewable changes (8)
- apps/playground/src/components/programs/forms/TrackerSystemFields.tsx
- apps/playground/src/components/programs/forms/EventSystemFields.tsx
- apps/playground/src/utils/formatProgramType.ts
- utils/devtools/src/ProgramRulesPanel.tsx
- apps/playground/src/pages/ProgramPlaceholderPage.tsx
- utils/devtools/src/RuleDevtoolsPanel.tsx
- apps/playground/src/hooks/useProgramHeader.ts
- apps/playground/src/components/programs/forms/OrganisationUnitSelectField.tsx
| g ? field('Residence', '', { ghost: true, ghostNote: 'Hidden by Hide residence for infants', badge: 'hide', badgeText: 'Hidden', badgeTitle: 'HIDEFIELD from rule: Hide residence for infants' }) : null, | ||
| field('Mother\u2019s name', '', { badge: 'mandatory', badgeText: 'Required by rule', badgeTitle: 'SETMANDATORYFIELD from rule: Require mother\u2019s name for newborns', ring: '#ffa902', help: 'Required while the child is under 2 months old.', helpFg: '#6f3205' }), | ||
| ].filter(Boolean) }, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Keep the hidden registration field visible as a badge-only row when ghosts are off.
Line 467 returns null when g is false, and the .filter(Boolean) then removes the row completely. The label and the hide badge disappear with it. stageSections (Lines 493-494) and eventSections (Lines 509-510) keep a badge-only field in the same situation, and Design handoff.md Line 205 specifies the badge on the label as the remaining trace. Mirror the stage branch here.
♻️ Proposed fix
- g ? field('Residence', '', { ghost: true, ghostNote: 'Hidden by Hide residence for infants', badge: 'hide', badgeText: 'Hidden', badgeTitle: 'HIDEFIELD from rule: Hide residence for infants' }) : null,
+ g ? field('Residence', '', { ghost: true, ghostNote: 'Hidden by Hide residence for infants', badge: 'hide', badgeText: 'Hidden', badgeTitle: 'HIDEFIELD from rule: Hide residence for infants' })
+ : field('Residence', '', { badge: 'hide', badgeText: 'Hidden', badgeTitle: 'HIDEFIELD from rule: Hide residence for infants' }),
field('Mother\u2019s name', '', { badge: 'mandatory', badgeText: 'Required by rule', badgeTitle: 'SETMANDATORYFIELD from rule: Require mother\u2019s name for newborns', ring: '`#ffa902`', help: 'Required while the child is under 2 months old.', helpFg: '`#6f3205`' }),
- ].filter(Boolean) },
+ ] },📝 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.
| g ? field('Residence', '', { ghost: true, ghostNote: 'Hidden by Hide residence for infants', badge: 'hide', badgeText: 'Hidden', badgeTitle: 'HIDEFIELD from rule: Hide residence for infants' }) : null, | |
| field('Mother\u2019s name', '', { badge: 'mandatory', badgeText: 'Required by rule', badgeTitle: 'SETMANDATORYFIELD from rule: Require mother\u2019s name for newborns', ring: '#ffa902', help: 'Required while the child is under 2 months old.', helpFg: '#6f3205' }), | |
| ].filter(Boolean) }, | |
| g ? field('Residence', '', { ghost: true, ghostNote: 'Hidden by Hide residence for infants', badge: 'hide', badgeText: 'Hidden', badgeTitle: 'HIDEFIELD from rule: Hide residence for infants' }) | |
| : field('Residence', '', { badge: 'hide', badgeText: 'Hidden', badgeTitle: 'HIDEFIELD from rule: Hide residence for infants' }), | |
| field('Mother\u2019s name', '', { badge: 'mandatory', badgeText: 'Required by rule', badgeTitle: 'SETMANDATORYFIELD from rule: Require mother\u2019s name for newborns', ring: '`#ffa902`', help: 'Required while the child is under 2 months old.', helpFg: '`#6f3205`' }), | |
| ] }, |
🤖 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/design/Program` Rules Playground App.dc.html around lines 467
- 469, Update the residence field branch near the hidden registration row so it
returns a badge-only field when ghosts are disabled instead of null. Mirror the
existing badge-only behavior used by the corresponding stageSections and
eventSections branches, preserving the current ghost rendering when g is enabled
and keeping the hide badge and label visible otherwise.
- EnrollmentRail: expose selected row state via aria-current - ProgramContextBar: don't pass '' as SingleSelectField's selected value - HiddenSectionPlaceholder: interpolate rule name into one translation unit instead of concatenating a translated fragment with a raw value - RuleFeedbackList: don't render a dangling ": " for text-only feedback, which has no value to append - ProgramRegistrationForm/ProgramEventForm: build one memoized rules-metadata object (with programStages) and reuse it for the label lookup, feedback list, and RulesPanel — fixes stage names showing as raw UIDs in the feedback list, and stops rebuilding rule/field/section maps every render - ProgramPage: "Reset playground" now remounts the active form via a reset key, so it actually clears typed field values and rail/event-draft state instead of only resetting org unit + date; distinguish a 404 from other query failures instead of always showing "Program not found" - D2Field: reuse the canonical WidgetProps type instead of a duplicate same-shaped D2FieldWidgetProps - RulesPanel: resolve tab labels at render time instead of module load so locale changes take effect; fix an inverted guard that made the screen-reader evaluation summary always report zero; correct the empty rules message now that the catalog isn't filtered by stage - useRuleEffectTrace test: hoist the tracker metadata fixture so it keeps a stable identity across renders instead of forcing rule-engine re-init - CLAUDE.md/docs/dev-tools.md: update references from the removed ProgramRulesPanel/RuleDevtoolsPanel to the merged RulesPanel Not applied: a finding on RuleAwareField's label-blanking removing the field's accessible name for assistive tech — a real issue, but fixing it correctly needs auditing/extending all D2*Field widgets to accept an explicit id/aria-labelledby, not a quick patch; flagged as a follow-up. Also not applied: a fix in the static design-prototype .dc.html file under apps/playground/design/, which is reference material excluded from lint/ build, not application source. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RuleAwareField blanks each widget's fieldConfig.label to avoid rendering a duplicate visible label, but @dhis2/ui's field components don't accept an aria-label/aria-labelledby prop — once label is falsy they render no label element at all, so the underlying input lost its only accessible name. Verified empirically (both live DOM and @dhis2-ui/input's source) that 11 of 13 widget kinds forward `name` straight onto the rendered element's `id`, while SingleSelectField/CheckboxField (boolean, trueOnly, select) have no id/name-based labelling mechanism in this @dhis2/ui version at all — a pre-existing upstream gap. A single `htmlFor` fix on our own label wouldn't cover those. Fix: keep our own visible label as the sole accessible name, and after render, point whichever interactive element the widget actually produced (input/textarea/the select's custom div) at it via aria-labelledby. This works uniformly across all widget kinds without touching any of the 13 D2*Field widget files. Radio-group fields (their own <fieldset>/<legend>) are left alone since that's already correctly labelled natively. Confirmed live for a text field, date field, select field, and checkboxes that the accessible name now resolves to the visible label text. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@docs/dev-tools.md`:
- Around line 156-174: Update the architecture statement describing the hooks
dependency to identify `@dhis2-form-utils/hooks` as a runtime dependency rather
than type-only, consistent with RulesPanel’s useFormStore() call. Keep the
surrounding dependency and bundling guidance unchanged.
- Around line 156-174: Update the RulesPanel example comment to accurately
describe its graph-state integration: state that it subscribes to
formStore.fieldStore and reads form.getValues() before passing data to
RuleGraphView, and remove the inaccurate references to useFieldState,
useSectionState, and useFormFeedback.
🪄 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: 0e763e9e-5afb-48f6-a075-7c1eb4a9f474
📒 Files selected for processing (14)
CLAUDE.mdapps/playground/i18n/en.potapps/playground/src/components/programs/EnrollmentRail.tsxapps/playground/src/components/programs/ProgramContextBar.tsxapps/playground/src/components/programs/forms/ProgramEventForm.tsxapps/playground/src/components/programs/forms/ProgramRegistrationForm.tsxapps/playground/src/components/rules/HiddenSectionPlaceholder.tsxapps/playground/src/components/rules/RuleAwareField.tsxapps/playground/src/components/rules/RuleFeedbackList.tsxapps/playground/src/pages/ProgramPage.tsxcomponents/dhis2-ui/src/fields/D2Field.tsxdocs/dev-tools.mdutils/devtools/src/RulesPanel.tsxutils/hooks/src/useRuleEffectTrace.test.tsx
🚧 Files skipped from review as they are similar to previous changes (12)
- apps/playground/src/components/programs/ProgramContextBar.tsx
- apps/playground/src/components/rules/HiddenSectionPlaceholder.tsx
- apps/playground/src/pages/ProgramPage.tsx
- apps/playground/i18n/en.pot
- apps/playground/src/components/rules/RuleFeedbackList.tsx
- utils/hooks/src/useRuleEffectTrace.test.tsx
- utils/devtools/src/RulesPanel.tsx
- apps/playground/src/components/programs/EnrollmentRail.tsx
- apps/playground/src/components/programs/forms/ProgramRegistrationForm.tsx
- apps/playground/src/components/programs/forms/ProgramEventForm.tsx
- components/dhis2-ui/src/fields/D2Field.tsx
- apps/playground/src/components/rules/RuleAwareField.tsx
- devtools/hooks dependency isn't type-only: RulesPanel calls useFormStore(), a runtime hook, not just types - RulesPanel's example comment named useFieldState/useSectionState/ useFormFeedback, none of which it actually calls — it subscribes to formStore.fieldStore directly and reads form.getValues() Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/playground/src/components/programs/ProgramListTable.tsx (1)
149-166: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUse a five-column span for table state and pagination rows.
The table defines five columns: Program, Type, Stages, Last updated, and the navigation icon.
colSpan="6"at Line 150, Line 166, and Line 179 can create an extra column and misalign the loading, empty-state, and pagination rows. Change these values tocolSpan="5".Proposed fix
-<DataTableCell colSpan="6"> +<DataTableCell colSpan="5">Also applies to: 177-193
🤖 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/components/programs/ProgramListTable.tsx` around lines 149 - 166, Update the colSpan values on the loading, empty-state, and pagination DataTableCell elements in ProgramListTable so they use five columns instead of six, preserving alignment with the table’s five defined columns.
🧹 Nitpick comments (1)
apps/playground/src/components/programs/ProgramListTable.tsx (1)
57-62: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd tests for keyboard row activation.
apps/playground/src/components/programs/ProgramListTable.test.tsxcurrently verifies only a mouse click. Add tests for Enter and Space on therole="button"cell.🤖 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/components/programs/ProgramListTable.tsx` around lines 57 - 62, Add tests in ProgramListTable.test.tsx covering keyboard activation of the role="button" cell: dispatch Enter and Space key events, verify each prevents default behavior, and confirm onProgramSelect is called with the selected program.
🤖 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/components/programs/ProgramListTable.tsx`:
- Around line 85-88: Update the stage DataTableCell and its corresponding
DataTableColumnHeader in ProgramListTable to use the same alignment, changing
the cell’s align setting to match the header’s existing right alignment.
---
Outside diff comments:
In `@apps/playground/src/components/programs/ProgramListTable.tsx`:
- Around line 149-166: Update the colSpan values on the loading, empty-state,
and pagination DataTableCell elements in ProgramListTable so they use five
columns instead of six, preserving alignment with the table’s five defined
columns.
---
Nitpick comments:
In `@apps/playground/src/components/programs/ProgramListTable.tsx`:
- Around line 57-62: Add tests in ProgramListTable.test.tsx covering keyboard
activation of the role="button" cell: dispatch Enter and Space key events,
verify each prevents default behavior, and confirm onProgramSelect is called
with the selected program.
🪄 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: ff12b82c-f5dc-4da0-8126-246d634b191e
📒 Files selected for processing (4)
.claude/launch.jsonapps/playground/src/components/programs/ProgramListFilters.tsxapps/playground/src/components/programs/ProgramListTable.tsxapps/playground/src/pages/ProgramListPage.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/playground/src/components/programs/ProgramListFilters.tsx
- apps/playground/src/pages/ProgramListPage.tsx
| <DataTableCell | ||
| onClick={handleSelect} | ||
| align="center" | ||
| className="w-30 tabular-nums" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Keep the stages header and values aligned consistently.
DataTableColumnHeader at Line 136 uses align="right", but the stage cell at Line 87 uses align="center". Apply the same alignment to both elements.
Also applies to: 135-140
🤖 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/components/programs/ProgramListTable.tsx` around lines 85
- 88, Update the stage DataTableCell and its corresponding DataTableColumnHeader
in ProgramListTable to use the same alignment, changing the cell’s align setting
to match the header’s existing right alignment.


Summary
Implements the design in
apps/playground/design/Design handoff.md: a redesigned program list, a merged tracker/event program page (context bar + enrollment rail + unified rule panel), and a rule-effect visualization system that shows which program rule hid, required, assigned, warned, or errored on each field/section — including "ghost" placeholders that keep hidden fields/sections in their grid slot instead of collapsing to nothing.@dhis2-form-utils/hooksexports (useRuleEffectTrace,useFieldRuleEffect,useSectionRuleEffect) generalize the rule-trace-to-target lookup that previously only lived inside devtools, so any consumer can ask "which rule caused this effect?"RuleAwareField/FormSectionCardcalluseFieldControl/useSectionStatedirectly instead ofD2Field/FormSection(which just returnnullwhen hidden), so hidden fields/sections render as dashed-hatch placeholders with the responsible rule's name.ProgramRulesPanel+RuleDevtoolsPanelare replaced by a singleRulesPanel(Rules / Trace / Graph tabs) in@dhis2-form-utils/devtools, with scope-aware 4-state rule cards (selected / firing-in-scope / idle-in-scope / out-of-scope).EnrollmentRail+TrackerProgramShellrender registration and every program stage (including repeatable-stage events) as independently-mounted forms, CSS-hidden rather than unmounted when not selected — so switching between stages/events never discards data already typed into another one.ProgramContextBar, so they're playground context rather than part of the rule-evaluation surface.apps/playground/design/**(design handoff reference material, including a generated design-system export bundle) from eslint, prettier, and git.Test plan
pnpm typecheck— clean across all packagespnpm lint— clean (root config now excludesapps/playground/design/**)pnpm --filter @dhis2-form-utils/hooks|devtools|rules|metadata test— all green (139 tests)usehooks-ts/Jest ESM suite-load failure, present onmainbefore this change)🤖 Generated with Claude Code
Summary by CodeRabbit