fix(company-input-v2): keep Use row on refocus, purge stale free-text - #292
Conversation
…exists
The Use "<typed>" row disappeared after the user committed a free-text
entry and re-focused the field, because filterOptions treated its own
prior {id:0,name} option as "already listed".
Two fixes to filterOptions:
- alreadyListed only counts real (id > 0) companies, not free-text.
- When params.inputValue is empty (passive refocus with no typing) and
the current value is a committed free-text, fall back to its name so
the Use row still appears.
Adds a regression test for the retype-same-text case.
After the user commits a free-text value via blur and starts a new
query, the previously-committed {id: 0} option lingered in the options
list until the debounced API response replaced them. In the meantime a
refocus would briefly show the stale entry ("ti" flashing while the
user was typing "tip").
The effect now purges any leftover free-text options synchronously the
moment normalizedValue or inputValue changes, before firing the request.
Adds a regression test that catches the stale entry mid-request.
…Company Mirrors the isExistingCompany predicate — pair reads more naturally as "predicate + finder for the same shape". Pure rename.
The X.trim().toLowerCase() comparison for company names appeared in three places (findExistingCompany, filterOptions, onBlur). Extracting a single namesMatch helper removes the duplication and gives the intent a name.
Moves the Use-row eligibility predicate out of the filterOptions callback for unit-testability. filterOptions now composes cleanly with the (later) getUseRowText helper. Adds four unit tests, including the id:0-doesnt-suppress-Use case that pins the recently-fixed regression.
Dedupes the findExistingCompany(opts, X) || { id: 0, name: X } pattern
that appeared in both onBlur and onChange. Both sites are answering the
same question — turn a typed string into either the canonical existing
company or a fresh free-text entry — and should agree on the answer.
…Name The ternary derivation exactly duplicates what getOptionName already does (handles string, company object, other → "").
Formalises the two-source resolution (active typing + committed free-text fallback) that filterOptions already needed. Reduces filterOptions to its two-line essence.
…ireChange in onChange The onChange handler's three-branch normalization (string, synthetic Use row, pass-through) is now a single helper call. The handler also uses the existing fireChange helper instead of hand-building the onChange envelope; matches the rest of the component and removes the onChange prop/handler name shadow.
… effect The effect callback now reads as three coherent steps: reset options, try to upgrade a stale free-text to its canonical version, done. The "is this a free-text? then look up the canonical" logic is now a named helper (findCanonicalUpgrade) instead of an inline conditional. Also collapses the six-line newOptions builder into one composed array.
|
Warning Review limit reached
Next review available in: 35 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: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughChangesCompany input matching and free-text flow
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant User
participant CompanyInputV2
participant queryRegistrationCompanies
participant Listbox
User->>CompanyInputV2: commit free-text on blur
CompanyInputV2->>CompanyInputV2: remove stale id 0 option
CompanyInputV2->>queryRegistrationCompanies: request company results
User->>CompanyInputV2: refocus before response
CompanyInputV2->>Listbox: render current options
queryRegistrationCompanies-->>CompanyInputV2: return API results
CompanyInputV2->>CompanyInputV2: upgrade to canonical company when matched
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 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: 3
🤖 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/inputs/__tests__/company-input-v2.test.js`:
- Around line 430-436: Strengthen the dropdown test after
fireEvent.mouseDown(input) by first asserting that the listbox or current “Use”
row is present, then retain the optionTexts absence assertions. Use the existing
input test’s visible dropdown/listbox symbol or role so the test fails when the
menu does not open instead of passing on an empty result.
In `@src/components/inputs/company-input-v2.js`:
- Around line 89-93: The getUseRowText fallback currently restores
normalizedValue.name whenever inputValue is empty, including after the user
clears the field. Update getUseRowText to distinguish an actively cleared input
from passive refocus, preserving the committed-name fallback only for passive
refocus and returning an empty string after clearing; add a regression test
covering clear-before-blur behavior.
- Around line 81-82: Update resolveCommittedCompany to route every string value
through resolveTypedCompany, and return null when the committed string is blank
after trimming. Preserve existing Company values and ensure fireChange receives
only a Company or null rather than a whitespace-only string.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: b7c76065-edea-4ea9-820d-25319e38fc44
📒 Files selected for processing (2)
src/components/inputs/__tests__/company-input-v2.test.jssrc/components/inputs/company-input-v2.js
| export const getUseRowText = (params, normalizedValue) => { | ||
| const typed = params.inputValue.trim(); | ||
| if (typed) return typed; | ||
| return isNewCompany(normalizedValue) ? normalizedValue.name.trim() : ""; | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
set -e
ast-grep outline src/components/inputs/company-input-v2.js --view expanded
printf '\n--- file excerpt 1 ---\n'
sed -n '1,180p' src/components/inputs/company-input-v2.js | cat -n
printf '\n--- file excerpt 2 ---\n'
sed -n '180,280p' src/components/inputs/company-input-v2.js | cat -nRepository: OpenStackweb/openstack-uicore-foundation
Length of output: 15753
🏁 Script executed:
set -e
printf 'Test files mentioning company-input-v2 or getUseRowText:\n'
rg -n "company-input-v2|getUseRowText|Use \"" src test . --glob '*.{js,jsx,ts,tsx}' || true
printf '\nCandidate test file outlines:\n'
fd -a "company-input-v2" src test . 2>/dev/null || trueRepository: OpenStackweb/openstack-uicore-foundation
Length of output: 3793
🏁 Script executed:
set -e
sed -n '330,500p' src/components/inputs/__tests__/company-input-v2.test.js | cat -nRepository: OpenStackweb/openstack-uicore-foundation
Length of output: 6865
Avoid reusing the committed name after the input is cleared. getUseRowText() falls back to normalizedValue.name whenever params.inputValue is empty, so deleting all text can briefly bring back Use "<old name>" until blur clears the value. Keep that fallback for passive refocus only, and add a clear-before-blur regression test.
🤖 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/inputs/company-input-v2.js` around lines 89 - 93, The
getUseRowText fallback currently restores normalizedValue.name whenever
inputValue is empty, including after the user clears the field. Update
getUseRowText to distinguish an actively cleared input from passive refocus,
preserving the committed-name fallback only for passive refocus and returning an
empty string after clearing; add a regression test covering clear-before-blur
behavior.
Two small hardening changes: - `resolveCommittedCompany` now normalises blank/whitespace-only strings to `null` instead of returning the raw string. Guarantees fireChange only ever receives a Company or null. - The flash-prevention test now asserts the listbox is open and the new Use "tip" row is present before checking that the stale "ti" is absent. Prevents the absence assertions from passing vacuously if the popup ever fails to reopen.
ref: https://app.clickup.com/t/86b9rnr4r
Follow-up to #290 addressing two dropdown regressions found during local verification.
Bugs fixed
1.
Use "<typed>"row disappeared after committing free-text and refocusing — thealreadyListedcheck treated the just-committed{id: 0, name}entry as a real match and suppressed the Use row. Also affected passive refocus (click back with no typing) because MUI resetsparams.inputValueto empty when the display value matches the selection.2. Stale free-text option flashed briefly on refocus after a new commit — the previously-committed
{id: 0}option lingered in the options list until the debounced API response replaced it. Refocus in that window showed the stale entry.Changes
Fixes (2 commits):
filterOptionsnow only counts real (id > 0) companies as "already listed" for the Use row check, and falls back to the committed free-text's name whenparams.inputValueis empty.{id: 0}options synchronously the momentnormalizedValuechanges, before firing the API request.Refactors on top (8 commits, no behavior change):
findExistingByName→findExistingCompany(mirrorsisExistingCompany).namesMatch— dedupes theX.trim().toLowerCase()comparison across three sites.shouldOfferUseRow— unit-testable predicate for the Use row.resolveTypedCompany— dedupes onBlur/onChange typed-string resolution.currentNameviagetOptionName.getUseRowText— formalises the active-typing + committed-free-text fallback.resolveCommittedCompany, usefireChangein onChange (removes prop/handler name shadow, dedupes with the rest of the component).findCanonicalUpgrade, simplify effect's options builder.Naming convention
The extracted helpers follow a consistent verb-prefix convention already in the file:
is*predicates,find*returns match or null,resolve*transforms input to a Company (always returns),get*computed getter,should*boolean decision.Tests
34/34 green. Each fix commit ships with an integration test that fails without the fix (verified by reverting each in isolation). The
shouldOfferUseRowextraction adds four unit tests including the id:0-doesnt-suppress-Use case that pins the regression.Summary by CodeRabbit