Skip to content

Conversation

@avazirna
Copy link
Contributor

@avazirna avazirna commented Jan 28, 2026

Product Description

This PR addresses an issue with the ComboboxWidget where, when duplicate options exist in the dropdown list, the returned value is always from the first option:

comboboxwidget_duplicates_issue.mp4

cross-request: dimagi/commcare-core#1516
Ticket: https://dimagi.atlassian.net/browse/SAAS-18514

Technical Summary

The root cause of this behavior was a design limitation: the ComboboxAdapter was based on String, so after a selection is made, it fetches the value by comparing the selected text with the text of each the question choices and returning the first match found. This fix makes the ComboboxAdapter to be based on a new model, ComboItem.

Safety Assurance

QA Plan

This will need to go through QA.

Labels and Review

  • Do we need to enhance the manual QA test coverage ? If yes, the "QA Note" label is set correctly
  • Does the PR introduce any major changes worth communicating ? If yes, the "Release Note" label is set and a "Release Note" is specified in PR description.
  • Risk label is set correctly
  • The set of people pinged as reviewers is appropriate for the level of risk of the change

@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

The change refactors combobox-related classes to use ComboItem instead of String across three files. ComboboxAdapter now extends ArrayAdapter, with constructors, getItem, filtering, and internal arrays converted to ComboItem[]. Combobox accepts and processes Vector and uses ComboItem.getDisplayText() where appropriate. ComboboxWidget tracks choices as Vector, adds selectedComboItem state, provides getComboItemChoicesWithEmptyFirstSlot, maps selection values to ComboItem, and updates answer extraction, clearing, and initialization to operate on ComboItem objects.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • OrangeAndGreen
  • conroy-ricketts
🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description covers the problem statement, root cause, and technical approach, but several required sections lack sufficient detail. Expand Safety Assurance section with: (1) how the fix was validated through testing; (2) specific automated test coverage that validates duplicate option handling; (3) detailed QA plan beyond stating 'needs QA'; also add Feature Flag section if applicable.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix duplicate options returning same value in ComboboxWidget' accurately describes the main change: refactoring ComboboxWidget to use ComboItem type instead of String, which enables proper handling of duplicate display options with different underlying values.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-duplicate-options-issue-in-combobox-widget

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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/org/commcare/views/Combobox.java (1)

100-106: Fix capitalization check after type migration.

choices is now Vector<ComboItem>, so choices.contains(enteredText) will never match. This changes behavior and can cause unnecessary resets. Prefer comparing display text (or rely solely on choicesAllLowerCase).

🛠️ Suggested fix
-        if (enteredText != null && !choices.contains(enteredText) &&
-                choicesAllLowerCase.contains(enteredText.toLowerCase())) {
-            int index = choicesAllLowerCase.indexOf(enteredText.toLowerCase());
-            setText(choices.get(index).getDisplayText());
-        }
+        if (enteredText != null && choicesAllLowerCase.contains(enteredText.toLowerCase())) {
+            int index = choicesAllLowerCase.indexOf(enteredText.toLowerCase());
+            String displayText = choices.get(index).getDisplayText();
+            if (!displayText.equals(enteredText)) {
+                setText(displayText);
+            }
+        }
🤖 Fix all issues with AI agents
In `@app/src/org/commcare/views/widgets/ComboboxWidget.java`:
- Around line 121-138: fillInPreviousAnswer currently assumes
getComboItemFromSelectionValue returns a non-null ComboItem and directly calls
selectedComboItem.getDisplayText(), which can NPE if choices changed; update
fillInPreviousAnswer to check the result of
getComboItemFromSelectionValue(selectionValue) for null before assigning to
selectedComboItem and calling comboBox.setText, and handle the null case (e.g.,
leave selectedComboItem null and skip setting comboBox text or set a safe
fallback) so comboBox.setText and selectedComboItem are only used when a valid
ComboItem is returned.
- Around line 147-158: getAnswer() currently only uses selectedComboItem (set on
click or prior fill) so typed input is ignored; change logic in
ComboboxWidget.getAnswer() to read the current text from the combo box when
selectedComboItem is null or not found in choiceComboItems, compare that text
against the available choices (choices vector / SelectChoice labels or values),
and if a match is found return a SelectOneData with that matching Selection; if
the text is empty return null, otherwise return InvalidData with the entered
text wrapped in SelectOneData. Refer to symbols:
comboBox.autoCorrectCapitalization(), selectedComboItem, choiceComboItems,
choices, SelectOneData, Selection, and InvalidData to locate and update the
code.

Comment on lines 147 to 162
comboBox.autoCorrectCapitalization();
String enteredText = comboBox.getText().toString();
if (choiceTexts.contains(enteredText)) {
int i = choiceTexts.indexOf(enteredText);
return new SelectOneData(new Selection(choices.elementAt(i)));
} else if ("".equals(enteredText)) {
return null;
if (selectedComboItem != null && choiceComboItems.contains(selectedComboItem)) {
int selectChoiceIndex = selectedComboItem.getSelectChoiceIndex();
return new SelectOneData(new Selection(choices.elementAt(selectChoiceIndex)));
} else {
return new InvalidData("The text entered is not a valid answer choice",
new SelectOneData(new Selection(enteredText)));
String enteredText = selectedComboItem!=null ? selectedComboItem.getDisplayText() : "";
if ("".equals(enteredText)) {
return null;
} else {
return new InvalidData("The text entered is not a valid answer choice",
new SelectOneData(new Selection(enteredText)));
}
Copy link

@coderabbitai coderabbitai bot Jan 28, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Typed entries are ignored unless user clicks a dropdown item.

getAnswer() relies on selectedComboItem, which is only set on click or previous-answer fill. If a user types a valid option (or edits after selection), this returns null/invalid. This is a functional regression.

🛠️ Suggested fix
-        comboBox.autoCorrectCapitalization();
-        if (selectedComboItem != null && choiceComboItems.contains(selectedComboItem)) {
+        comboBox.autoCorrectCapitalization();
+        String enteredText = comboBox.getText().toString();
+        if (selectedComboItem == null || !enteredText.equals(selectedComboItem.getDisplayText())) {
+            selectedComboItem = null;
+            for (ComboItem item : choiceComboItems) {
+                if (item.getDisplayText().equals(enteredText)) {
+                    selectedComboItem = item;
+                    break;
+                }
+            }
+        }
+        if (selectedComboItem != null && choiceComboItems.contains(selectedComboItem)) {
             int selectChoiceIndex = selectedComboItem.getSelectChoiceIndex();
             return new SelectOneData(new Selection(choices.elementAt(selectChoiceIndex)));
         } else {
-            String enteredText = selectedComboItem!=null ? selectedComboItem.getDisplayText() : "";
             if ("".equals(enteredText)) {
                 return null;
             } else {
🤖 Prompt for AI Agents
In `@app/src/org/commcare/views/widgets/ComboboxWidget.java` around lines 147 -
158, getAnswer() currently only uses selectedComboItem (set on click or prior
fill) so typed input is ignored; change logic in ComboboxWidget.getAnswer() to
read the current text from the combo box when selectedComboItem is null or not
found in choiceComboItems, compare that text against the available choices
(choices vector / SelectChoice labels or values), and if a match is found return
a SelectOneData with that matching Selection; if the text is empty return null,
otherwise return InvalidData with the entered text wrapped in SelectOneData.
Refer to symbols: comboBox.autoCorrectCapitalization(), selectedComboItem,
choiceComboItems, choices, SelectOneData, Selection, and InvalidData to locate
and update the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This crossed my mind and I'm glad you flagged it too. We can return the first option when the text matches a valid option

Copy link

Choose a reason for hiding this comment

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

@avazirna Great, I'm glad we're aligned on this! The "first match" approach makes sense, especially given that this PR is about supporting duplicate options.

Just to clarify the logic: when getAnswer() is called, if selectedComboItem is null or the entered text doesn't match it, the code should iterate through choiceComboItems to find the first ComboItem whose getDisplayText() matches the entered text. If found, return the corresponding SelectOneData; otherwise, follow the existing invalid/empty logic.

This ensures that both click-selection and typed-entry work correctly, and when there are duplicate display texts, the first matching option is consistently used.

Let me know if you'd like me to plan the implementation or if you've got it covered! 🚀

@avazirna avazirna marked this pull request as draft January 28, 2026 13:41
@avazirna avazirna force-pushed the fix-duplicate-options-issue-in-combobox-widget branch from 2446234 to 78f3ca4 Compare January 28, 2026 14:13
@avazirna
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@avazirna avazirna changed the title Support duplicate options in ComboboxWidget Fix duplicate options returning same value in ComboboxWidget Jan 29, 2026
@avazirna avazirna marked this pull request as ready for review January 29, 2026 09:47
@avazirna
Copy link
Contributor Author

@damagatchi retest this please

}

private ComboItem getComboItemFromSelectionValue(String selectionValue) {
for (ComboItem cboIt: choiceComboItems) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: can we name this variable comboItem since cboIt is not a common contraction

@avazirna avazirna self-assigned this Jan 30, 2026
@conroy-ricketts
Copy link
Contributor

@avazirna can we add the Safety Story section to the PR description describing how you've personally tested these changes and are confident that this solves the issue

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.

3 participants