Skip to content

fix(select): comprehensive hardening for option deduplication and new option handling - #44894

Merged
aminghadersohi merged 12 commits into
apache:masterfrom
FrancescoCastaldi:fix/asyncselect-drop-duplicate-new-option
Oct 30, 2026
Merged

aminghadersohi merged 12 commits into
apache:masterfrom
FrancescoCastaldi:fix/asyncselect-drop-duplicate-new-option

Conversation

@FrancescoCastaldi

@FrancescoCastaldi FrancescoCastaldi commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

SUMMARY

Comprehensive hardening suite for Select and AsyncSelect option handling, custom option deduplication, and visual indicators (resolving #44873).

In AsyncSelect with allowNewOptions={true} (such as the dashboard picker in Explore's "Save chart" modal), handleOnSearch introduces an optimistic isNewOption entry ({ label: typed, value: typed, isNewOption: true }) whenever the typed title is not yet present in currently loaded options (e.g., while waiting for the asynchronous search request to resolve or when existing dashboards span subsequent pages).

Previously, both mergeData and the page === 0 search merge in fetchPage deduplicated incoming server options strictly by value (!dataValues.has(opt.value)). Because the optimistic entry's value is the typed label string whereas backend options have numeric IDs, the optimistic entry was preserved alongside the server option even when titles matched. As filed in #44873 (and encountered while fixing Playwright tests in #44856), this displayed duplicate rows with identical labels; selecting the optimistic row silently created an empty dashboard with a duplicate title instead of associating the chart with the existing dashboard.

Key Changes:

  • Server Deduplication (AsyncSelect): In AsyncSelect, filter out isNewOption entries in both mergeData and fetchPage (page 0) whenever the server response contains an option with a matching label or value via hasOption.
  • Option Lookup & Equality Helpers (utils.tsx): Hardened isEqual, getOption, and hasOption for bidirectional string/number and trimmed comparisons; added isNewOption helper and unit tests.
  • Parity in Select: Aligned synchronous Select.tsx to check against both loaded and initial options before generating temporary custom entries.
  • Visual Distinction for Custom Options: Added StyledNewOptionBadge rendering a subtle [new] tag in optionRender for custom options in both Select and AsyncSelect.
  • Deselect & Clear Cleanup: Cleanly purge unselected temporary options upon clear, and match both label and value during removal on deselect.
  • TypeScript Types: Strengthened CustomLabeledValue with explicit isNewOption?: boolean typing.
  • Test Coverage & Stories: Added regression tests in AsyncSelect.test.tsx for Save chart modal can create a duplicate dashboard when the typed title matches an existing one #44873, edge-case tests for paste deduplication and clear cleanup, and interactive Storybook stories (WithAllowNewOptions).

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

  • Dropdown cleanly renders a single option when remote search results match the typed label (drops duplicate optimistic entry).
  • Optimistic newly-typed options feature a subtle themed [new] tag in the dropdown.

TESTING INSTRUCTIONS

  1. Open a chart in Explore and click Save.
  2. Type an existing dashboard title into the dashboard select.
  3. Observe the dropdown options: once search results arrive, only the existing dashboard is shown; no duplicate optimistic "create new" entry remains.
  4. Run unit tests:
    npm test superset-frontend/packages/superset-ui-core/src/components/Select/

ADDITIONAL INFORMATION

Comment on lines +388 to +392
if (
previousOption.isNewOption &&
(hasOption(previousOption.label as V, data, true) ||
hasOption(previousOption.value as V, data, true))
) {

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.

Suggestion: hasOption checks only top-level entries, so matching children inside grouped options are missed and the optimistic entry remains beside the fetched option.

Assessment: 🟠 Major · 🔁 Occurrence: Rarely · 🏷️ Logic error

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.tsx
**Line:** 388:392
**Comment:**
	*Logic Error: `hasOption` checks only top-level entries, so matching children inside grouped `options` are missed and the optimistic entry remains beside the fetched option.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Resolved in commit e7c9b0e0de: updated getOption in Select/utils.tsx to recursively inspect child options when encountering option groups (isObject(x) && Array.isArray(x.options)), ensuring that matching child options within grouped structures are identified and optimistic duplicates are dropped properly.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of af604c5.

isRedundantNewOption now checks each group's options children for a matching label before deciding the optimistic option is not redundant.

If that's not right, unresolve this thread and CodeAnt will leave it open.

@bito-code-review

Copy link
Copy Markdown
Contributor

The flagged issue is correct. The current hasOption implementation likely only checks top-level options, failing to detect matches within grouped options (where options are nested under a options array). To resolve this, hasOption needs to be updated to recursively search through nested options arrays if they exist.

Would you like me to check the rest of the comments on this PR and implement fixes for them as well?

superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.tsx

const hasOption = (value: V, options: SelectOptionsType, recursive = false): boolean => {
  return options.some(option => {
    if (option.value === value || option.label === value) return true;
    if (recursive && 'options' in option && Array.isArray(option.options)) {
      return hasOption(value, option.options as SelectOptionsType, true);
    }
    return false;
  });
};

@bito-code-review bito-code-review 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.

Code Review Agent Run #cfc715

Actionable Suggestions - 1
  • superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.tsx - 1
Additional Suggestions - 1
  • superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.tsx - 1
    • loose-equality label match · Line 390-391
      `hasOption(label, data, true)` matches both value and label using loose equality (`isEqual` returns `actualA == actualB`). A new option typed as a numeric string (e.g. '42') is dropped when the server returns an option with value `42` but a different label, even though no label match exists — contradicting the 'match for the label' intent.
Review Details
  • Files reviewed - 2 · Commit Range: 20be903..20be903
    • superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.test.tsx
    • superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.tsx
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

Comment on lines +388 to +394
if (
previousOption.isNewOption &&
(hasOption(previousOption.label as V, data, true) ||
hasOption(previousOption.value as V, data, true))
) {
return 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.

duplicated new-option filter

The new-option-dropping predicate is duplicated: mergeData (lines 388-394) and the page === 0 preservedNew filter (lines 493-497) both implement 'drop isNewOption when hasOption(label/value, data, true)'. Extract one helper used by both paths to avoid divergence.

Code Review Run #cfc715


Should Bito avoid suggestions like this for future reviews? (Manage Rules)

  • Yes, avoid them

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Resolved in commit 7019744718: extracted the shared predicate isRedundantNewOption in AsyncSelect.tsx and unified the duplicate check across mergeData and the initial page search response filter.

@codecov

codecov Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.74359% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.70%. Comparing base (65db213) to head (af604c5).
⚠️ Report is 12 commits behind head on master.

Files with missing lines Patch % Lines
...rset-ui-core/src/components/Select/AsyncSelect.tsx 83.87% 5 Missing ⚠️
...s/superset-ui-core/src/components/Select/utils.tsx 91.89% 3 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master   #44894   +/-   ##
=======================================
  Coverage   84.70%   84.70%           
=======================================
  Files        3069     3069           
  Lines      205597   205657   +60     
  Branches    48327    48366   +39     
=======================================
+ Hits       174148   174204   +56     
- Misses      28157    28161    +4     
  Partials     3292     3292           
Flag Coverage Δ
javascript 80.94% <89.74%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@FrancescoCastaldi
FrancescoCastaldi force-pushed the fix/asyncselect-drop-duplicate-new-option branch from 20be903 to 16d248c Compare October 2, 2026 14:05
@pull-request-size pull-request-size Bot added size/L and removed size/M labels Oct 2, 2026
@FrancescoCastaldi FrancescoCastaldi changed the title fix(select): drop optimistic new option when fetched options match label fix(select): comprehensive hardening for option deduplication and new option handling Oct 2, 2026
@FrancescoCastaldi

FrancescoCastaldi commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Hi team,

Here is a summary of the hardening and fixes included in this PR for #44873:

  • Root Cause & Server Deduplication (AsyncSelect): In AsyncSelect, handleOnSearch inserts an optimistic option while waiting for the search response. We now filter out isNewOption entries in both mergeData and fetchPage (page 0) whenever the server response contains an option with a matching label or value. This directly prevents duplicate rows and avoids creating duplicate dashboards when saving charts in Explore.
  • Option Lookup & Equality Helpers (utils.tsx): Hardened isEqual, getOption, and hasOption to support bidirectional string/number and trimmed comparisons, and introduced an isNewOption helper with full unit test coverage.
  • Parity in Select: Aligned synchronous Select.tsx with the same deduplication checks against both loaded and initial option lists.
  • Visual Distinction for Custom Options: Introduced a subtle [new] badge (StyledNewOptionBadge) in optionRender for optimistic custom options in both Select and AsyncSelect, making custom entries clearly identifiable.
  • Deselect & Clear Cleanup: Ensured unselected temporary options are purged on clear, and properly removed by label and value on deselect.
  • Test Coverage & Stories: Added regression tests for Save chart modal can create a duplicate dashboard when the typed title matches an existing one #44873 in AsyncSelect.test.tsx, edge-case tests for paste deduplication and clear cleanup, and interactive Storybook stories (WithAllowNewOptions).

All quality checks (oxlint with 0 errors and oxfmt) pass cleanly across the package.

@bito-code-review

bito-code-review Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #32516

Actionable Suggestions - 0
Additional Suggestions - 3
  • superset-frontend/packages/superset-ui-core/src/components/Select/Select.tsx - 2
    • Grouped-option dedup gap · Line 591-591
      `hasOption(unquotedSearch, initialOptions, true)` delegates to `getOption`, which iterates top-level entries only (utils.tsx:78-83). For grouped `{label, options}` lists the nested labels are never matched, so a typed term equal to a nested option still creates a duplicate `isNewOption` entry. Consider flattening `initialOptions` first.
    • Duplicated badge/dedup logic · Line 1074-1083
      The `isNewOption` badge block (1074-1083) and the deselect dedup filter (528-530) are copied verbatim into `AsyncSelect.tsx` (932-941, 352-354). Both components already share `utils.tsx`; extracting these into a helper keeps the parallel implementations from diverging.
  • superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.test.tsx - 1
    • Ineffective test assertion · Line 1261-1289
      This test never exercises the behavior it names. `rerender` with the same `loadOptions` reference does not fire the `useEffect([options])` reset, so no paginated `mergeData` runs; the final `expect(loadOptions).toBeDefined()` is trivially true. Scroll to page 1 and assert the optimistic option is actually dropped.
Review Details
  • Files reviewed - 10 · Commit Range: e5b93ab..16d248c
    • superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.stories.tsx
    • superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.test.tsx
    • superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.tsx
    • superset-frontend/packages/superset-ui-core/src/components/Select/Select.stories.tsx
    • superset-frontend/packages/superset-ui-core/src/components/Select/Select.test.tsx
    • superset-frontend/packages/superset-ui-core/src/components/Select/Select.tsx
    • superset-frontend/packages/superset-ui-core/src/components/Select/styles.tsx
    • superset-frontend/packages/superset-ui-core/src/components/Select/types.ts
    • superset-frontend/packages/superset-ui-core/src/components/Select/utils.test.ts
    • superset-frontend/packages/superset-ui-core/src/components/Select/utils.tsx
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • Eslint (Linter) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@github-actions github-actions Bot added the requires:rebase Requires rebasing on top of current master label Oct 3, 2026
@codeant-ai-for-open-source

codeant-ai-for-open-source Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

CodeAnt  CodeAnt PR Risk

🟢  Low Risk

  • The PR appears safe to merge; fetched grouped options are checked for matching child labels before optimistic entries are retained.

Assessed commit: af604c560a27

@github-actions github-actions Bot removed the requires:rebase Requires rebasing on top of current master label Oct 6, 2026
@netlify

netlify Bot commented Oct 6, 2026

Copy link
Copy Markdown

✅ Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 28d7f98
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6ac4f04ac1cfae000879eea5
😎 Deploy Preview https://deploy-preview-44894--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@FrancescoCastaldi

Copy link
Copy Markdown
Contributor Author

Hi @rusackas @sadpandajoe,

Pushed commits e7c9b0e0de and 7019744718 addressing review findings:

  1. Option groups traversal:
    Updated getOption in Select/utils.tsx to recursively inspect child options arrays when grouped option items are encountered (isObject(x) && Array.isArray(x.options)). This ensures matching child options within option groups are detected and optimistic duplicates are dropped properly.

  2. Unified new-option filter helper:
    Extracted the shared isRedundantNewOption predicate in AsyncSelect.tsx, consolidating the duplicate detection across mergeData and the initial page search response filter.

All TypeScript checks and unit tests pass cleanly. Whenever you have a moment, please take a look.

@coderabbitai review
@bito-code-review

FrancescoCastaldi added a commit to FrancescoCastaldi/superset that referenced this pull request Oct 13, 2026

@aminghadersohi aminghadersohi 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.

CI: the 2 red playwright-tests-gaq lanes aren't yours - they need superset_test_config_gaq.py, added to master 2026-10-06 after this branch's base, and the job checks out your head. Merge master to clear; they're also continue-on-error. Real blocker is lint-frontend, which fails at tsc, not lint.

FrancescoCastaldi added a commit to FrancescoCastaldi/superset that referenced this pull request Oct 13, 2026
apache#44894)

1. Harmonize SelectValue union with LabeledValue in types.ts to preserve consumer contracts and resolve TS2322.

2. Type story args with Omit<AsyncSelectProps, 'ref'> in AsyncSelect.stories.tsx to prevent ref spread mismatch.

3. Refine isEqual equality invariant in utils.tsx to distinguish empty string and whitespace while preserving whitespace trimming and numeric zero equality.

4. Add regression unit tests in utils.test.ts for whitespace boundary conditions and numeric zero.
@FrancescoCastaldi

Copy link
Copy Markdown
Contributor Author

Hi @aminghadersohi,

I have pushed commit 844d017 addressing all three review comments on this PR:

  1. SelectValue harmonization (types.ts):
    Harmonized SelectValue to explicitly include LabeledValue and LabeledValue[]. This preserves external consumer contracts (such as SelectAsyncControl/index.tsx) and resolves TS2322 without modifying consumer files, while fully satisfying the AsyncSelect and Select test suites.

  2. Storybook props typing (AsyncSelect.stories.tsx):
    Typed the WithAllowNewOptions story arguments with Omit<AsyncSelectProps, 'ref'>, eliminating the ref mismatch between BaseSelectProps and AsyncSelectRef during arg spreading.

  3. Refined equality invariant in isEqual (utils.tsx):
    Conditioned trimmed string comparisons on trimmedA.length > 0 && trimmedA === trimmedB, and added explicit guards for numeric zero comparisons against empty or whitespace strings. This ensures:

  • isEqual('', ' ') === false
  • isEqual(' ', ' ') === false
  • isEqual('', '') === true and isEqual(' ', ' ') === true
  • isEqual(' Sales ', 'Sales') === true (preserving intended label deduplication)
  • isEqual(0, '') === false while isEqual(0, '0') === true

Validation and test evidence:

  • Test suite: 224/224 unit tests passing across constants, utils, Select, and AsyncSelect.
  • Static typing: TypeScript compilation check completes with 0 errors (0 TS2322).
  • Dedicated regression tests added to utils.test.ts covering whitespace and numeric boundary conditions.

The branch fix/asyncselect-drop-duplicate-new-option is updated and ready for your re-review.

Best regards,
Francesco Castaldi

@aminghadersohi aminghadersohi 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.

Verified here: npm run type RC=0 (0 errors), lint-frontend green, Select suites 293/293. Only reds are the 2 playwright-tests-gaq lanes - still the missing superset_test_config_gaq.py from the stale base, not this code, not required. Trimmed-label equality (' Sales '=='Sales') stays open by design.

@FrancescoCastaldi

Copy link
Copy Markdown
Contributor Author

@aminghadersohi Thank you for the detailed verification!

Glad that npm run type, lint-frontend, and all 293 Select test suites are clean and passing. Understood on the non-blocking playwright gaq lanes due to the base.

Everything is in place and verified green on frontend lint, types, and Jest test suites. Ready for final review and merge when you have a moment, thank you!

FrancescoCastaldi added a commit to FrancescoCastaldi/superset that referenced this pull request Oct 28, 2026
FrancescoCastaldi added a commit to FrancescoCastaldi/superset that referenced this pull request Oct 28, 2026
apache#44894)

1. Harmonize SelectValue union with LabeledValue in types.ts to preserve consumer contracts and resolve TS2322.

2. Type story args with Omit<AsyncSelectProps, 'ref'> in AsyncSelect.stories.tsx to prevent ref spread mismatch.

3. Refine isEqual equality invariant in utils.tsx to distinguish empty string and whitespace while preserving whitespace trimming and numeric zero equality.

4. Add regression unit tests in utils.test.ts for whitespace boundary conditions and numeric zero.
@FrancescoCastaldi
FrancescoCastaldi force-pushed the fix/asyncselect-drop-duplicate-new-option branch from 844d017 to 4ff49e2 Compare October 28, 2026 15:01
@FrancescoCastaldi

Copy link
Copy Markdown
Contributor Author

Hi @aminghadersohi,

The branch has been rebased on the latest master to pick up superset_test_config_gaq.py for the Playwright GAQ test suite. All merge conflicts in utils.tsx, AsyncSelect.tsx, and test suites have been resolved, and the PR is cleanly mergeable.

FrancescoCastaldi added a commit to FrancescoCastaldi/superset that referenced this pull request Oct 28, 2026
…nents (apache#44894)

- Export isNewOption type guard helper in utils.tsx
- Import isNewOption in Select.tsx, AsyncSelect.tsx, and utils.test.ts
- Resolves ReferenceError in SelectFilterPlugin and unit tests

Signed-off-by: Francesco Castaldi <info@francescocastaldi.it>

@aminghadersohi aminghadersohi 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.

Two missing StyledNewOptionBadge imports crash any dropdown that shows a new option, and the narrowed isNewOption guard breaks type-checking; fixes inline.

FrancescoCastaldi and others added 11 commits October 29, 2026 22:59
apache#44894)

1. Harmonize SelectValue union with LabeledValue in types.ts to preserve consumer contracts and resolve TS2322.

2. Type story args with Omit<AsyncSelectProps, 'ref'> in AsyncSelect.stories.tsx to prevent ref spread mismatch.

3. Refine isEqual equality invariant in utils.tsx to distinguish empty string and whitespace while preserving whitespace trimming and numeric zero equality.

4. Add regression unit tests in utils.test.ts for whitespace boundary conditions and numeric zero.
…nents (apache#44894)

- Export isNewOption type guard helper in utils.tsx
- Import isNewOption in Select.tsx, AsyncSelect.tsx, and utils.test.ts
- Resolves ReferenceError in SelectFilterPlugin and unit tests

Signed-off-by: Francesco Castaldi <info@francescocastaldi.it>
…pache#44894)

Signed-off-by: Francesco Castaldi <info@francescocastaldi.it>
@FrancescoCastaldi
FrancescoCastaldi force-pushed the fix/asyncselect-drop-duplicate-new-option branch from d98f30c to 5f33ef3 Compare October 29, 2026 22:07
@FrancescoCastaldi

Copy link
Copy Markdown
Contributor Author

Summary of Changes addressing Review Feedback

I have rebased onto current upstream/master and addressed all feedback points raised by @aminghadersohi in commit 5f33ef3d68a:

  1. Missing Component Imports:

    • Added StyledNewOptionBadge to ./styles imports in both AsyncSelect.tsx and Select.tsx.
    • Added missing imports (isEqual, getOption, hasOption) from ./utils into utils.test.ts.
  2. Type Safety & Signatures:

    • Widened value parameter in getOption and hasOption (utils.tsx) to V | CustomLabeledValue so object lookups compile cleanly under strict TypeScript checking without TS2345.
    • Adjusted isNewOption in utils.tsx to return boolean (isObject(option) && Boolean(option.isNewOption)), preventing incorrect type narrowing on opt.label and opt.value.
    • Replaced raw property access !opt.isNewOption with !isNewOption(opt) in both AsyncSelect.tsx and Select.tsx to safeguard against primitive option shapes.
  3. Label Matching Invariant:

    • Refactored isRedundantNewOption in AsyncSelect.tsx to match incoming options strictly against label (including recursive checking of grouped option children) using isEqual(..., 'label'). This prevents numeric IDs or option values from erroneously matching typed text (e.g. typing 12 no longer collides with a record having value: 12 and label Report 12).
  4. Integration Test Suite:

    • Updated drops optimistic new option when paginated mergeData contains matching label in AsyncSelect.test.tsx to trigger pagination by dispatching a scroll event on the virtual-list container, verifying the page 1 fetch, asserting the duplicate optimistic option is dropped, and ensuring the server option is retained without the new option badge.

cc @aminghadersohi for re-review.

@github-actions github-actions Bot removed the requires:rebase Requires rebasing on top of current master label Oct 29, 2026

@aminghadersohi aminghadersohi 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.

The new label check calls lodash isEqual, so it fails type-checking and never drops the duplicate new option; fixes inline.

…p preservation, and trim key (apache#44894)

Signed-off-by: Francesco Castaldi <info@francescocastaldi.it>
@FrancescoCastaldi

Copy link
Copy Markdown
Contributor Author

Review Follow-Up & CI Remediation

All review comments have been addressed in commit af604c560a2:

  1. utilsIsEqual for Option Comparison (AsyncSelect.tsx):

    • Replaced lodash isEqual with utilsIsEqual(..., 'label') in AsyncSelect.tsx, fixing the TS2554 3-argument error and ensuring optimistic new options are reliably dropped when matches exist in incoming data or grouped options.
  2. Pagination Test Data (AsyncSelect.test.tsx):

    • Set totalCount: 20 across page returns so that hasMoreData remains true for the first page, allowing the scroll event to trigger the page 1 fetch without timing out.
  3. Prop Preservation for Missing Values (Select.tsx):

    • Updated missing value mapping to isLabeledValue(opt) ? opt : { value: opt, label: String(opt) } so custom attributes such as disabled and title are preserved rather than dropped.
  4. Restricting Whitespace Trim to Labels (utils.tsx & utils.test.ts):

    • Restricted whitespace trimming logic strictly to key === 'label'. Raw string values like 'Sales ' and 'Sales' remain distinct values, preserving selection in multi-mode and ensuring onChange fires.
  5. Style & Linter Audit:

    • Verified all modified files adhere to the strict 88-column limit.

@aminghadersohi, the PR has been updated and is ready for your re-review whenever you have a moment. Thank you!

@aminghadersohi
aminghadersohi merged commit a57c3d3 into apache:master Oct 30, 2026
81 checks passed
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.

Save chart modal can create a duplicate dashboard when the typed title matches an existing one

2 participants