This is an automated email from the ASF dual-hosted git repository.

vavila pushed a commit to branch fix/slack-search
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 1afdcf07acdded0419756b3f80aaddb144298386
Author: Vitor Avila <vitor.av...@preset.io>
AuthorDate: Mon Aug 11 11:44:53 2025 -0300

    fix: Slack channels and Color Palettes search
---
 .../src/components/Select/Select.test.tsx          | 113 +++++++++++++++++++++
 .../src/components/Select/Select.tsx               |  24 ++++-
 2 files changed, 134 insertions(+), 3 deletions(-)

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 b6c2af4a81..93608a5031 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
@@ -1047,6 +1047,119 @@ test('typing and deleting the last character for a new 
option displays correctly
   jest.useRealTimers();
 });
 
+describe('grouped options search', () => {
+  const GROUPED_OPTIONS = [
+    {
+      label: 'Male',
+      options: OPTIONS.filter(option => option.gender === 'Male'),
+    },
+    {
+      label: 'Female',
+      options: OPTIONS.filter(option => option.gender === 'Female'),
+    },
+  ];
+
+  it('searches within grouped options and shows matching groups', async () => {
+    render(<Select {...defaultProps} options={GROUPED_OPTIONS} />);
+    await open();
+
+    await type('John');
+
+    expect(await findSelectOption('John')).toBeInTheDocument();
+    expect(await findSelectOption('Johnny')).toBeInTheDocument();
+    expect(screen.queryByText('Female')).not.toBeInTheDocument();
+    expect(screen.queryByText('Olivia')).not.toBeInTheDocument();
+    expect(screen.getByText('Male')).toBeInTheDocument();
+    expect(screen.queryByText('Female')).not.toBeInTheDocument();
+  });
+
+  it('shows multiple groups when search matches both', async () => {
+    render(<Select {...defaultProps} options={GROUPED_OPTIONS} />);
+    await open();
+
+    await type('er');
+
+    expect(screen.getByText('Male')).toBeInTheDocument();
+    expect(screen.getByText('Female')).toBeInTheDocument();
+    expect(await findSelectOption('Oliver')).toBeInTheDocument();
+    expect(await findSelectOption('Cher')).toBeInTheDocument();
+    expect(await findSelectOption('Her')).toBeInTheDocument();
+  });
+
+  it('handles case-insensitive search in grouped options', async () => {
+    render(<Select {...defaultProps} options={GROUPED_OPTIONS} />);
+    await open();
+
+    await type('EMMA');
+
+    expect(await findSelectOption('Emma')).toBeInTheDocument();
+    expect(screen.getByText('Female')).toBeInTheDocument();
+    expect(screen.queryByText('Male')).not.toBeInTheDocument();
+  });
+
+  it('shows no options when search matches nothing in any group', async () => {
+    render(<Select {...defaultProps} options={GROUPED_OPTIONS} />);
+    await open();
+
+    await type('xyz123');
+
+    expect(screen.queryByText('Male')).not.toBeInTheDocument();
+    expect(screen.queryByText('Female')).not.toBeInTheDocument();
+    expect(
+      screen.getByText(NO_DATA, { selector: '.ant-empty-description' }),
+    ).toBeInTheDocument();
+  });
+
+  it('works in multiple selection mode with grouped options', async () => {
+    render(
+      <Select {...defaultProps} options={GROUPED_OPTIONS} mode="multiple" />,
+    );
+    await open();
+
+    await type('John');
+
+    await userEvent.click(await findSelectOption('John'));
+
+    // Clear search and search for female name
+    await clearTypedText();
+    await type('Emma');
+    await userEvent.click(await findSelectOption('Emma'));
+
+    // Both should be selected
+    const values = await findAllSelectValues();
+    expect(values).toHaveLength(2);
+    expect(values[0]).toHaveTextContent('John');
+    expect(values[1]).toHaveTextContent('Emma');
+  });
+
+  it('preserves group structure when not searching', async () => {
+    render(<Select {...defaultProps} options={GROUPED_OPTIONS} />);
+    await open();
+
+    expect(screen.getByText('Male')).toBeInTheDocument();
+    expect(screen.getByText('Female')).toBeInTheDocument();
+    expect(await findSelectOption('John')).toBeInTheDocument();
+    expect(await findSelectOption('Emma')).toBeInTheDocument();
+  });
+
+  it('handles empty groups gracefully', async () => {
+    const optionsWithEmptyGroup = [
+      ...GROUPED_OPTIONS,
+      {
+        label: 'Empty Group',
+        options: [],
+      },
+    ];
+
+    render(<Select {...defaultProps} options={optionsWithEmptyGroup} />);
+    await open();
+
+    await type('John');
+    expect(await findSelectOption('John')).toBeInTheDocument();
+    expect(screen.queryByText('Empty Group')).not.toBeInTheDocument();
+  });
+});
+
 /*
  TODO: Add tests that require scroll interaction. Needs further investigation.
  - Fetches more data when scrolling and more data is available
diff --git 
a/superset-frontend/packages/superset-ui-core/src/components/Select/Select.tsx 
b/superset-frontend/packages/superset-ui-core/src/components/Select/Select.tsx
index 48cb2cfe17..adfa1f7bcb 100644
--- 
a/superset-frontend/packages/superset-ui-core/src/components/Select/Select.tsx
+++ 
b/superset-frontend/packages/superset-ui-core/src/components/Select/Select.tsx
@@ -373,9 +373,27 @@ const Select = forwardRef(
         setSelectOptions(updatedOptions);
       }
 
-      const filteredOptions = updatedOptions.filter(
-        (option: AntdLabeledValue) => handleFilterOption(search, option),
-      );
+      const filteredOptions = updatedOptions
+        .map((option: any) => {
+          /*
+          If it's a group, filter its nested options and only return it
+          if it has matching options
+          */
+          if ('options' in option && Array.isArray(option.options)) {
+            const filteredGroupOptions = option.options.filter(
+              (subOption: AntdLabeledValue) =>
+                handleFilterOption(search, subOption),
+            );
+            return filteredGroupOptions.length > 0
+              ? { ...option, options: filteredGroupOptions }
+              : null;
+          }
+
+          return handleFilterOption(search, option as AntdLabeledValue)
+            ? option
+            : null;
+        })
+        .filter((option): option is AntdLabeledValue => option !== null);
 
       setVisibleOptions(filteredOptions);
       setInputValue(searchValue);

Reply via email to