From c85bde9c34ecd7fb4b12688852780476107c2bda Mon Sep 17 00:00:00 2001 From: Francesco Castaldi Date: Fri, 2 Oct 2026 16:00:24 +0200 Subject: [PATCH 01/12] refactor(select): strengthen TypeScript types and interfaces for Select options --- .../superset-ui-core/src/components/Select/types.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/superset-frontend/packages/superset-ui-core/src/components/Select/types.ts b/superset-frontend/packages/superset-ui-core/src/components/Select/types.ts index 15f66280df7c..5b1c937bfbef 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Select/types.ts +++ b/superset-frontend/packages/superset-ui-core/src/components/Select/types.ts @@ -31,7 +31,12 @@ export type V = string | number | null | undefined; export type LabeledValue = AntdLabeledValue; -export type CustomLabeledValue = { label?: ReactNode; value?: V }; +export type CustomLabeledValue = { + label?: ReactNode; + value?: V; + /** Flag identifying optimistic custom options created when allowNewOptions is enabled */ + isNewOption?: boolean; +}; export type AntdProps = AntdSelectProps; From 21e0aaed7259d362a3625b8fcd56dea16a0f270f Mon Sep 17 00:00:00 2001 From: Francesco Castaldi Date: Fri, 2 Oct 2026 16:01:33 +0200 Subject: [PATCH 02/12] test(select): add comprehensive edge-case tests for tokenization, paste, and search clearing --- .../components/Select/AsyncSelect.test.tsx | 34 +++++++++++++++++++ .../src/components/Select/Select.test.tsx | 17 ++++++++++ 2 files changed, 51 insertions(+) diff --git a/superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.test.tsx b/superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.test.tsx index 657eedd5d8f0..5344408dd0a4 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.test.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.test.tsx @@ -592,6 +592,40 @@ test('clear all the values', async () => { expect(values.length).toBe(0); }); +test('cleans up temporary new option when clearAll is called in allowNewOptions mode', async () => { + const onClear = jest.fn(); + render( + , + ); + await clearAll(); + expect(onClear).toHaveBeenCalled(); + const values = await findAllSelectValues(); + expect(values.length).toBe(0); +}); + +test('deduplicates pasted values against loaded options when allowNewOptions is enabled', async () => { + render(); + const input = getElementByClassName('.ant-select-input'); + const paste = createEvent.paste(input, { + clipboardData: { + getData: () => `${OPTIONS[0].label},${OPTIONS[0].label},BrandNewItem`, + }, + }); + fireEvent(input, paste); + const values = await findAllSelectValues(); + expect(values.length).toBe(2); + expect(values[0]).toHaveTextContent(OPTIONS[0].label); + expect(values[1]).toHaveTextContent('BrandNewItem'); +}); + test('does not add a new option if allowNewOptions is false', async () => { render(); await open(); diff --git a/superset-frontend/packages/superset-ui-core/src/components/Select/Select.test.tsx b/superset-frontend/packages/superset-ui-core/src/components/Select/Select.test.tsx index 8d1e4ced85af..5f725cca0457 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Select/Select.test.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Select/Select.test.tsx @@ -592,6 +592,23 @@ test('clear all the values', async () => { expect(values.length).toBe(0); }); +test('cleans up temporary new option when clearAll is called in allowNewOptions mode', async () => { + const onClear = jest.fn(); + render( + ); await open(); From e5a7e3af4df7f7af67fb6803ef69717bbe5e99c1 Mon Sep 17 00:00:00 2001 From: Francesco Castaldi Date: Fri, 2 Oct 2026 16:04:28 +0200 Subject: [PATCH 03/12] docs(select): update Storybook stories and documentation for allowNewOptions behavior --- .../components/Select/AsyncSelect.stories.tsx | 48 +++++++++++++++++++ .../src/components/Select/Select.stories.tsx | 23 +++++++++ 2 files changed, 71 insertions(+) diff --git a/superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.stories.tsx index 862319a2a1d9..942b6c0d8bd7 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.stories.tsx @@ -318,3 +318,51 @@ AsynchronousSelect.argTypes = { }, }, }; + +const createMockUserFetcher = + (responseTime = 0.3) => + ( + search: string, + page: number, + pageSize: number, + ): Promise => { + const username = search.trim().toLowerCase(); + const all = USERS.filter( + u => !username || u.toLowerCase().includes(username), + ); + const totalCount = all.length; + const start = page * pageSize; + const deleteCount = + start + pageSize < totalCount ? pageSize : totalCount - start; + const pageData = all.slice(start, start + deleteCount).map(u => ({ + label: u, + value: u, + })); + return new Promise(resolve => { + setTimeout(() => { + resolve({ data: pageData, totalCount }); + }, responseTime * 1000); + }); + }; + +/** + * Demonstrates allowNewOptions in AsyncSelect. Optimistic custom options appear + * with a [new] badge and are automatically dropped if the server returns a matching + * option upon search completion. + */ +export const WithAllowNewOptions = (args: AsyncSelectProps) => ( +
+ +
+); + +WithAllowNewOptions.args = { + ...AsynchronousSelect.args, + allowNewOptions: true, + placeholder: 'Type a name or create a new one...', +}; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Select/Select.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/Select/Select.stories.tsx index 238f9c5edeee..b19eaeefc204 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Select/Select.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Select/Select.stories.tsx @@ -468,3 +468,26 @@ AdvancedPlayground.argTypes = { control: { type: 'number' }, }, }; + +/** + * Demonstrates the allowNewOptions feature, allowing users to type and create + * custom options that are rendered with a subtle [new] visual badge, while + * ensuring existing options are deduplicated against typed terms. + */ +export const WithAllowNewOptions = (args: SelectProps) => ( +
+