Skip to content

✨ Add generic type-safe autocomplete with radio variant (ENG-648)#16539

Closed
Jacobjeevan wants to merge 1 commit into
developfrom
eng-648/generic-autocomplete
Closed

✨ Add generic type-safe autocomplete with radio variant (ENG-648)#16539
Jacobjeevan wants to merge 1 commit into
developfrom
eng-648/generic-autocomplete

Conversation

@Jacobjeevan

@Jacobjeevan Jacobjeevan commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

🥅 What are the changes?

Introduces a generic, type-safe replacement for the string-only Autocomplete.

  • New GenericAutocomplete<T> (src/components/ui/generic-autocomplete.tsx)
    • Default generic (T = { label; value }) reproduces the existing Autocomplete prop contract exactly, so existing usages migrate by renaming the component only.
    • Custom option types via getOptionValue / getOptionLabel, with optional renderOption (list item) and renderSelected (trigger) for rich content.
    • New enableRadio variant rendering a radio group (reuses RadioInput), gated by a module const RADIO_MAX_OPTIONS = 5 — radio renders only when enabled and options.length <= 5, otherwise it falls back to the combobox.
    • Preserves desktop popover + mobile drawer, server-side search (filter={false}), freeInput, clear button, shortcutId, onOpenChange.
  • Deprecates Autocomplete via a @deprecated JSDoc pointing at the new component. The other ~20 usages migrate over time — untouched here.
  • Refactors HealthcareServiceSelector to use GenericAutocomplete (custom HealthcareServiceReadSpec), dropping its hand-rolled Command/Popover/Drawer (~167 fewer lines) while keeping the icon + colored indicator, extra_details subtitle, server search, clear, and mobile drawer.

🧪 Testing

  • tsc --noEmit, ESLint (changed files), and npm run build all pass.
  • Migration path: swap import Autocompleteimport GenericAutocomplete and the JSX tag name; no prop changes for default string usage.

Non-goals

  • Not migrating the remaining Autocomplete usages (done incrementally).
  • No multi-select changes (separate multi-select.tsx).

Summary by CodeRabbit

  • New Features

    • Added a flexible, type-safe autocomplete component with customizable labels, rendering, search, clearing, loading states, and empty-state messaging.
    • Added optional free-text input and radio-style selection for smaller option lists.
    • Improved responsive behavior with tailored mobile and desktop selection interfaces.
    • Updated healthcare service selection with consistent service icons and integrated loading and search feedback.
  • Documentation

    • Marked the existing autocomplete component as deprecated in favor of the new replacement.

…r HealthcareServiceSelector

- Add src/components/ui/generic-autocomplete.tsx: GenericAutocomplete<T> with
  a default generic matching the old Autocomplete contract (rename-only
  migration), custom getOptionValue/getOptionLabel/renderOption/renderSelected,
  and an enableRadio variant gated by RADIO_MAX_OPTIONS=5.
- Mark Autocomplete as @deprecated pointing to the new component.
- Refactor HealthcareServiceSelector to use GenericAutocomplete.
@Jacobjeevan Jacobjeevan requested review from a team and Copilot July 10, 2026 21:39
@github-actions

Copy link
Copy Markdown

⚠️ Merge Checklist Incomplete

Thank you for your contribution! To help us review your PR efficiently, please complete the merge checklist in your PR description.

Your PR will be reviewed once you have marked the appropriate checklist items.

To update the checklist:

  • Change - [ ] to - [x] for completed items
  • Only check items that are relevant to your PR
  • Leave items unchecked if they don't apply

The checklist helps ensure code quality, testing coverage, and documentation are properly addressed.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Autocomplete migration

Layer / File(s) Summary
Generic autocomplete contract and state
src/components/ui/generic-autocomplete.tsx
Adds generic option and prop contracts, selection state, free-input and search handling, clearing, and radio-mode rendering.
Generic autocomplete rendering
src/components/ui/generic-autocomplete.tsx
Adds shared command content with loading and empty states, plus mobile Drawer and desktop Popover layouts.
Healthcare service selector integration
src/pages/Facility/services/HealthcareServiceSelector.tsx, src/components/ui/autocomplete.tsx
Migrates service selection to GenericAutocomplete, adds service icon renderers, updates loading wiring, and documents the previous component as deprecated.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the changes, testing, and non-goals, but it doesn't follow the required template or include issue/tagging details. Rewrite it to match the template: add the Proposed Changes section with Fixes #issue_number, include Tagging, and complete the Merge Checklist.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a generic type-safe autocomplete with a radio variant.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch eng-648/generic-autocomplete

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new generic, type-safe GenericAutocomplete<T> UI component (with an optional radio-group rendering mode for small option sets), deprecates the legacy string-only Autocomplete, and refactors HealthcareServiceSelector to use the new component to reduce bespoke UI code.

Changes:

  • Added GenericAutocomplete<T> with configurable option extractors/renderers, mobile drawer + desktop popover behavior, and a radio variant gated by RADIO_MAX_OPTIONS.
  • Deprecated legacy Autocomplete via JSDoc pointing consumers to GenericAutocomplete.
  • Refactored HealthcareServiceSelector to use GenericAutocomplete and custom rendering for service icon/details.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/pages/Facility/services/HealthcareServiceSelector.tsx Replaced bespoke popover/drawer selector with GenericAutocomplete using custom renderers for service icon and metadata.
src/components/ui/generic-autocomplete.tsx Added new generic autocomplete component, including optional radio-mode rendering for small option counts.
src/components/ui/autocomplete.tsx Marked legacy Autocomplete as deprecated in favor of GenericAutocomplete.

Comment on lines +72 to +83
return (
<GenericAutocomplete<HealthcareServiceReadSpec>
options={services?.results ?? []}
value={selected?.id ?? ""}
onChange={(id) =>
onSelect(services?.results.find((s) => s.id === id) ?? null)
}
onOpenChange={setOpen}
onSearch={setSearchValue}
isLoading={isFetching}
filter={false}
className="min-w-60 justify-start"
Comment on lines +34 to +38
/**
* Minimum option count above which the radio variant falls back to the
* combobox. Radio buttons are only rendered when `enableRadio` is set AND the
* number of options is within this threshold.
*/

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 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 `@src/components/ui/generic-autocomplete.tsx`:
- Around line 116-118: The default values for placeholder, inputPlaceholder, and
noOptionsMessage in the autocomplete component are hardcoded English. Remove
these literal defaults by making them undefined and translating them at each
callsite, or resolve them through the component’s i18n translation function
using appropriate keys.
- Line 314: Add a stable id to the CommandList or popup element in the generic
autocomplete, then set matching aria-controls attributes on both mobile and
desktop combobox triggers alongside their existing role and aria-expanded props.

In `@src/pages/Facility/services/HealthcareServiceSelector.tsx`:
- Around line 74-82: Update the options passed to GenericAutocomplete in
HealthcareServiceSelector so the preselected service is included alongside
services?.results, avoiding duplicates when it is already present. This ensures
the current value can render its label and icon immediately on mount.
- Around line 28-44: Validate styling_metadata.careIcon against the available
icon data before passing it to CareIcon in ServiceIcon; only call getIconName
for a recognized icon name, otherwise use the existing "d-health-worker"
fallback, preventing invalid keys from causing a lookup error.
🪄 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: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 387fbba4-c3cb-49c5-ba22-9dd80ce08a76

📥 Commits

Reviewing files that changed from the base of the PR and between 89a4aee and 856ac14.

📒 Files selected for processing (3)
  • src/components/ui/autocomplete.tsx
  • src/components/ui/generic-autocomplete.tsx
  • src/pages/Facility/services/HealthcareServiceSelector.tsx

Comment on lines +116 to +118
placeholder = "Select...",
inputPlaceholder = "Search option...",
noOptionsMessage = "No options found",

Copy link
Copy Markdown
Contributor

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

Default placeholder strings are hardcoded English.

placeholder, inputPlaceholder, and noOptionsMessage default to untranslated literals ("Select...", "Search option...", "No options found"). When a consumer omits these props, untranslated text is shown to users. Prefer defaulting to undefined and translating at the callsite, or resolving via t() inside the component with i18n keys.

As per coding guidelines: "All literal strings must use i18next for multi-language support in healthcare interfaces."

🤖 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 `@src/components/ui/generic-autocomplete.tsx` around lines 116 - 118, The
default values for placeholder, inputPlaceholder, and noOptionsMessage in the
autocomplete component are hardcoded English. Remove these literal defaults by
making them undefined and translating them at each callsite, or resolve them
through the component’s i18n translation function using appropriate keys.

Source: Coding guidelines

}
variant="outline"
ref={ref}
role="combobox"

Copy link
Copy Markdown
Contributor

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="src/components/ui/generic-autocomplete.tsx"

# Show an outline first, then the relevant line ranges around the reported triggers and popup/listbox.
python3 - <<'PY'
from pathlib import Path
path = Path("src/components/ui/generic-autocomplete.tsx")
lines = path.read_text().splitlines()
for start, end in [(250, 410)]:
    print(f"\n--- {path}:{start}-{end} ---")
    for i in range(start, min(end, len(lines)) + 1):
        print(f"{i:4d}: {lines[i-1]}")
PY

Repository: ohcnetwork/care_fe

Length of output: 6579


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
path = Path("src/components/ui/generic-autocomplete.tsx")
lines = path.read_text().splitlines()

# Print the top imports and the commandContent definition area.
for start, end in [(1, 170), (170, 250)]:
    print(f"\n--- {path}:{start}-{end} ---")
    for i in range(start, min(end, len(lines)) + 1):
        print(f"{i:4d}: {lines[i-1]}")
PY

Repository: ohcnetwork/care_fe

Length of output: 8972


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
path = Path("src/components/ui/command.tsx")
lines = path.read_text().splitlines()
print(f"--- {path} ---")
for i, line in enumerate(lines, 1):
    print(f"{i:4d}: {line}")
PY

Repository: ohcnetwork/care_fe

Length of output: 6255


Add aria-controls to both combobox triggers. In src/components/ui/generic-autocomplete.tsx, the mobile and desktop buttons expose role="combobox" and aria-expanded but never reference the CommandList/popup, so the trigger isn’t associated with the option list. Give the list a stable id and point both triggers at it.

🧰 Tools
🪛 React Doctor (0.5.8)

[error] 314-314: Screen reader users can't tell the state of this combobox without its required ARIA props, so add aria-controls.

Add every required aria-* attribute so assistive tech can expose the role's state correctly.

(role-has-required-aria-props)

🤖 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 `@src/components/ui/generic-autocomplete.tsx` at line 314, Add a stable id to
the CommandList or popup element in the generic autocomplete, then set matching
aria-controls attributes on both mobile and desktop combobox triggers alongside
their existing role and aria-expanded props.

Sources: Coding guidelines, Linters/SAST tools

Comment on lines +28 to +44
const getIconName = (name: string): DuoToneIconName =>
`d-${name}` as DuoToneIconName;

const ServiceIcon = ({ service }: { service: HealthcareServiceReadSpec }) => (
<div className="relative size-6 rounded-sm flex items-center justify-center">
<ColoredIndicator
id={service.id}
className="absolute inset-0 rounded-sm opacity-20"
/>
<CareIcon
icon={
service.styling_metadata?.careIcon
? getIconName(service.styling_metadata.careIcon)
: "d-health-worker"
}
className="size-4 relative z-1"
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm careIcon value format (prefixed vs bare) and available duo-tone keys
fd -t f 'DuoTonePaths.json' -x sh -c 'echo "== {} =="; jq -r "keys[]" {} | grep -i "health-worker"'
rg -nP "careIcon" -g '*.ts' -g '*.tsx' -C2

Repository: ohcnetwork/care_fe

Length of output: 215


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' src/CAREUI/icons/CareIcon.tsx
printf '\n---\n'
rg -n "careIcon" src -g '*.ts' -g '*.tsx'
printf '\n---\n'
rg -n "styling_metadata" src -g '*.ts' -g '*.tsx'

Repository: ohcnetwork/care_fe

Length of output: 7535


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' src/pages/Facility/services/HealthcareServiceSelector.tsx
printf '\n---\n'
sed -n '1,260p' src/components/GenericAutocomplete/GenericAutocomplete.tsx
printf '\n---\n'
sed -n '1,220p' src/pages/Facility/settings/healthcareService/HealthcareServiceForm.tsx

Repository: ohcnetwork/care_fe

Length of output: 3923


Guard careIcon before passing it to CareIcon
getIconName() casts arbitrary strings to DuoToneIconName, but CareIcon indexes icon data by that key. A bad styling_metadata.careIcon will throw here; validate the name and fall back to "d-health-worker".

🤖 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 `@src/pages/Facility/services/HealthcareServiceSelector.tsx` around lines 28 -
44, Validate styling_metadata.careIcon against the available icon data before
passing it to CareIcon in ServiceIcon; only call getIconName for a recognized
icon name, otherwise use the existing "d-health-worker" fallback, preventing
invalid keys from causing a lookup error.

Comment on lines +74 to +82
options={services?.results ?? []}
value={selected?.id ?? ""}
onChange={(id) =>
onSelect(services?.results.find((s) => s.id === id) ?? null)
}
onOpenChange={setOpen}
onSearch={setSearchValue}
isLoading={isFetching}
filter={false}

Copy link
Copy Markdown
Contributor

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find callers that pass a non-null `selected` on mount
rg -nP '<HealthcareServiceSelector' -A6 --type=tsx

Repository: ohcnetwork/care_fe

Length of output: 184


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== HealthcareServiceSelector definition ==="
sed -n '1,220p' src/pages/Facility/services/HealthcareServiceSelector.tsx

echo
echo "=== Call sites ==="
rg -n --glob '*.tsx' 'HealthcareServiceSelector' src/pages src/components src | sed -n '1,120p'

Repository: ohcnetwork/care_fe

Length of output: 5787


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' src/pages/Facility/services/HealthcareServiceSelector.tsx

printf '\n=== usage ===\n'
rg -n --glob '*.tsx' 'HealthcareServiceSelector' src | sed -n '1,120p'

Repository: ohcnetwork/care_fe

Length of output: 4784


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== src/components/Schedule/ResourceSelector.tsx ==="
sed -n '1,180p' src/components/Schedule/ResourceSelector.tsx

echo
echo "=== src/pages/Facility/settings/activityDefinition/ActivityDefinitionForm.tsx (around selector usage) ==="
sed -n '900,980p' src/pages/Facility/settings/activityDefinition/ActivityDefinitionForm.tsx

Repository: ohcnetwork/care_fe

Length of output: 6520


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file=$(git ls-files | rg 'generic-autocomplete\.(ts|tsx|js|jsx)$' | head -n 1)
echo "=== GenericAutocomplete file: $file ==="
sed -n '1,260p' "$file"

Repository: ohcnetwork/care_fe

Length of output: 7780


Seed the selected service into options on mount. GenericAutocomplete only renders the label/icon when it can find the current value in options, so a prefilled service shows the placeholder until the popover loads.

🤖 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 `@src/pages/Facility/services/HealthcareServiceSelector.tsx` around lines 74 -
82, Update the options passed to GenericAutocomplete in
HealthcareServiceSelector so the preselected service is included alongside
services?.results, avoiding duplicates when it is already present. This ensures
the current value can render its label and icon immediately on mount.

@greptile-apps

greptile-apps Bot commented Jul 10, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces GenericAutocomplete<T>, a type-safe replacement for the string-only Autocomplete, and migrates HealthcareServiceSelector to use it (removing ~167 lines of hand-rolled Command/Popover/Drawer code). The new component adds getOptionValue/getOptionLabel accessors, optional renderOption/renderSelected slots, and an enableRadio variant that falls back to combobox when options exceed RADIO_MAX_OPTIONS.

  • GenericAutocomplete faithfully replicates the existing Autocomplete contract so existing usages can migrate by rename alone; the radio path and custom-render hooks are new capabilities on top.
  • HealthcareServiceSelector is cleanly refactored but its custom renderOption ignores the isSelected parameter, losing the check-mark indicator that was present in the original.
  • The radio branch evaluates options.length at render time without considering isLoading, which can cause an empty radio group to flash during async data fetches.

Confidence Score: 3/5

The new component itself is well-structured, but the HealthcareServiceSelector refactor has a functional regression where users can no longer see which service is currently selected in the open dropdown.

The missing isSelected check in HealthcareServiceSelector's custom renderOption is a real UI regression — the check mark that confirmed the active selection in the original code is gone. This affects every user who opens the healthcare service dropdown with a pre-selected value.

src/pages/Facility/services/HealthcareServiceSelector.tsx — the renderOption callback needs to use the isSelected argument to restore the selection indicator.

Important Files Changed

Filename Overview
src/components/ui/generic-autocomplete.tsx New generic type-safe autocomplete with radio variant; solid implementation but the radio branch ignores isLoading and default placeholder strings are not i18n'd.
src/pages/Facility/services/HealthcareServiceSelector.tsx Refactored to use GenericAutocomplete — clean reduction in boilerplate, but custom renderOption ignores the isSelected parameter, dropping the check-mark indicator for the active selection.
src/components/ui/autocomplete.tsx Only change is adding a @deprecated JSDoc pointing to the new component — no functional changes.

Reviews (1): Last reviewed commit: "✨ Add generic type-safe autocomplete; de..." | Re-trigger Greptile

Comment on lines +96 to 110
renderOption={(service) => (
<div className="flex items-center gap-2 w-full">
<ServiceIcon service={service} />
<div className="flex flex-col min-w-0 flex-1">
<span className="truncate text-sm font-medium" title={service.name}>
{service.name}
</span>
{service.extra_details && (
<span className="text-xs text-gray-500 truncate">
{service.extra_details}
</span>
)}
</div>
<span className="truncate">{selected.name}</span>
</div>
) : (
<span className="text-gray-400">{t("select_healthcare_service")}</span>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Selected item loses its check mark indicator

The renderOption callback receives (option: T, isSelected: boolean) but this implementation only uses the first argument — isSelected is silently ignored. In the original code the selected service showed a CheckIcon when selected?.id === service.id. With the custom renderOption here the dropdown list has no visual indicator of the currently selected service, so users can't tell which item is active without looking at the trigger button.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +219 to +234
// Radio variant — only when enabled and within the option threshold.
if (enableRadio && options.length <= RADIO_MAX_OPTIONS) {
return (
<RadioInput
options={options.map((option) => ({
label: getOptionLabel(option),
value: getOptionValue(option),
}))}
value={value}
onValueChange={onChange}
disabled={disabled}
aria-invalid={props["aria-invalid"]}
className={radioClassName}
/>
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Radio variant can render as an empty group during async loads

The radio/combobox mode decision is based on options.length at render time. When an async caller passes isLoading={true} with an initially-empty options array, 0 <= RADIO_MAX_OPTIONS evaluates to true and an empty RadioInput is rendered while data is in flight. If the fetched data then comes back with more than RADIO_MAX_OPTIONS items the component silently switches to combobox mode mid-session. Consider gating the radio branch on !isLoading as well to avoid the empty flash and the abrupt layout change.

Comment on lines +116 to +118
placeholder = "Select...",
inputPlaceholder = "Search option...",
noOptionsMessage = "No options found",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Default strings bypass the i18n system

The project uses react-i18next for all user-visible text, but these three defaults are hardcoded English strings. Any usage of GenericAutocomplete that relies on the defaults will be untranslated. The t instance is already available inside the function; moving the defaults to resolved translation keys would keep the component consistent with the rest of the codebase.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@github-actions

Copy link
Copy Markdown

🎭 Playwright Test Results

Status: ❌ Failed
Test Shards: 3

Metric Count
Total Tests 331
✅ Passed 330
❌ Failed 1
⏭️ Skipped 0

📊 Detailed results are available in the playwright-final-report artifact.

Run: #10104

@Jacobjeevan Jacobjeevan deleted the eng-648/generic-autocomplete branch July 11, 2026 01:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants