Skip to content

fix(company-input-v2): keep Use row on refocus, purge stale free-text - #292

Merged
smarcet merged 11 commits into
mainfrom
fix/company-input-v2-use-row-persistence
Jul 15, 2026
Merged

fix(company-input-v2): keep Use row on refocus, purge stale free-text#292
smarcet merged 11 commits into
mainfrom
fix/company-input-v2-use-row-persistence

Conversation

@gcutrini

@gcutrini gcutrini commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

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 — the alreadyListed check 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 resets params.inputValue to 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):

  • filterOptions now only counts real (id > 0) companies as "already listed" for the Use row check, and falls back to the committed free-text's name when params.inputValue is empty.
  • Effect purges any leftover {id: 0} options synchronously the moment normalizedValue changes, before firing the API request.

Refactors on top (8 commits, no behavior change):

  • Rename findExistingByNamefindExistingCompany (mirrors isExistingCompany).
  • Extract namesMatch — dedupes the X.trim().toLowerCase() comparison across three sites.
  • Extract shouldOfferUseRow — unit-testable predicate for the Use row.
  • Extract resolveTypedCompany — dedupes onBlur/onChange typed-string resolution.
  • Simplify onBlur currentName via getOptionName.
  • Extract getUseRowText — formalises the active-typing + committed-free-text fallback.
  • Extract resolveCommittedCompany, use fireChange in onChange (removes prop/handler name shadow, dedupes with the rest of the component).
  • Extract 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 shouldOfferUseRow extraction adds four unit tests including the id:0-doesnt-suppress-Use case that pins the regression.

Summary by CodeRabbit

  • Bug Fixes
    • Improved company-name matching to ignore differences in capitalization and surrounding spaces.
    • Prevented stale free-text options from briefly reappearing when reopening the company list.
    • Improved handling of typed company names, including upgrading free-text entries when a matching company is found.
    • Prevented duplicate or inconsistent company options during selection, clearing, blur, and autofill actions.
  • Tests
    • Expanded coverage for matching, free-text filtering, and company input regression scenarios.

gcutrini added 10 commits July 15, 2026 12:27
…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.
@gcutrini
gcutrini requested a review from smarcet July 15, 2026 15:42
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@gcutrini, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

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

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

How do review limits work?

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

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

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5c06329c-47bf-4d06-8ecf-9b112fccfa47

📥 Commits

Reviewing files that changed from the base of the PR and between 701f309 and 3cbf01d.

📒 Files selected for processing (2)
  • src/components/inputs/__tests__/company-input-v2.test.js
  • src/components/inputs/company-input-v2.js
📝 Walkthrough

Walkthrough

Changes

Company input matching and free-text flow

Layer / File(s) Summary
Matching helpers and Use-row contracts
src/components/inputs/company-input-v2.js, src/components/inputs/__tests__/company-input-v2.test.js
Adds shared case-insensitive name matching, renames the company lookup helper, excludes id === 0 entries, and expands Use-row tests.
Free-text resolution and API upgrade
src/components/inputs/company-input-v2.js
Adds helpers for typed-value resolution, Use-row labels, committed-value normalization, canonical upgrades, stale free-text removal, and filtered options.
Commit handling and stale-option regression
src/components/inputs/company-input-v2.js, src/components/inputs/__tests__/company-input-v2.test.js
Updates blur/change handling and adds coverage ensuring stale free-text options are removed before a pending API response returns.

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

Possibly related PRs

Suggested reviewers: jpmaxman, smarcet

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
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately captures the two main fixes: keeping the Use row on refocus and purging stale free-text options.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/company-input-v2-use-row-persistence

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between ef998ef and 701f309.

📒 Files selected for processing (2)
  • src/components/inputs/__tests__/company-input-v2.test.js
  • src/components/inputs/company-input-v2.js

Comment thread src/components/inputs/__tests__/company-input-v2.test.js
Comment thread src/components/inputs/company-input-v2.js
Comment on lines +89 to +93
export const getUseRowText = (params, normalizedValue) => {
const typed = params.inputValue.trim();
if (typed) return typed;
return isNewCompany(normalizedValue) ? normalizedValue.name.trim() : "";
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 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 -n

Repository: 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 || true

Repository: 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 -n

Repository: 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.

@smarcet smarcet left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@smarcet
smarcet merged commit 97ec224 into main Jul 15, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants