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

rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 8be430014e5 fix(dashboard): make Clear All clear filters that have 
default values (#42111)
8be430014e5 is described below

commit 8be430014e511849359b051c3e542103b5473d9a
Author: jesperct <[email protected]>
AuthorDate: Wed Jul 29 02:05:51 2026 -0300

    fix(dashboard): make Clear All clear filters that have default values 
(#42111)
---
 .../nativeFilters/FilterBar/FilterBar.test.tsx     | 89 +++++++++++++++++++++-
 .../components/nativeFilters/FilterBar/index.tsx   | 19 +++--
 .../components/Range/RangeFilterPlugin.test.tsx    | 31 +++++++-
 .../filters/components/Range/RangeFilterPlugin.tsx | 10 ++-
 4 files changed, 133 insertions(+), 16 deletions(-)

diff --git 
a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx
 
b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx
index e62d3c99e87..6ec20f96267 100644
--- 
a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx
+++ 
b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/FilterBar.test.tsx
@@ -547,7 +547,7 @@ test('Clear All stages filter_select clear without 
dispatching until Apply', asy
   });
   expect(updateDataMaskSpy).toHaveBeenCalledWith(filterId, {
     id: filterId,
-    filterState: { value: undefined, validateStatus: undefined },
+    filterState: { value: null, validateStatus: undefined },
     extraFormData: {},
   });
   updateDataMaskSpy.mockRestore();
@@ -685,12 +685,95 @@ test('Clear All + Apply only dispatches for filters 
present in dataMask', async
   expect(updateDataMaskSpy).toHaveBeenCalledTimes(1);
   expect(updateDataMaskSpy).toHaveBeenCalledWith(idInMask, {
     id: idInMask,
-    filterState: { value: undefined, validateStatus: undefined },
+    filterState: { value: null, validateStatus: undefined },
     extraFormData: {},
   });
   updateDataMaskSpy.mockRestore();
 });
 
+test('Clear All in horizontal bar does not re-apply default values', async () 
=> {
+  fetchMock.post(
+    'glob:*/api/v1/chart/data',
+    {
+      result: [
+        {
+          data: [{ test_column: 'East' }, { test_column: 'West' }],
+          colnames: ['test_column'],
+          coltypes: [1],
+        },
+      ],
+    },
+    { name: 'horizontal-clear-chart-data' },
+  );
+  const filterId = 'NATIVE_FILTER-horizontal-default';
+  const updateDataMaskSpy = jest.spyOn(dataMaskActions, 'updateDataMask');
+  const filterWithDefault = createFilter({
+    id: filterId,
+    name: 'Region',
+    filterType: 'filter_select',
+    targets: [{ datasetId: 7, column: { name: 'test_column' } }],
+    defaultDataMask: {
+      filterState: { value: ['East'] },
+      extraFormData: {
+        filters: [{ col: 'test_column', op: 'IN', val: ['East'] }],
+      },
+    },
+    chartsInScope: [18],
+  });
+  const stateHorizontal = {
+    ...stateWithoutNativeFilters,
+    dashboardInfo: {
+      id: 1,
+      dash_edit_perm: true,
+      filterBarOrientation: FilterBarOrientation.Horizontal,
+      metadata: {
+        native_filter_configuration: [filterWithDefault],
+        chart_configuration: {},
+      },
+    },
+    dashboardState: {
+      ...stateWithoutNativeFilters.dashboardState,
+      activeTabs: ['ROOT_ID'],
+    },
+    dataMask: {
+      [filterId]: createDataMask(filterId, ['East'], {
+        filters: [{ col: 'test_column', op: 'IN', val: ['East'] }],
+      }),
+    },
+    nativeFilters: {
+      filters: { [filterId]: filterWithDefault },
+      filtersState: {},
+    },
+  };
+
+  render(<FilterBar orientation={FilterBarOrientation.Horizontal} />, {
+    initialState: stateHorizontal,
+    useDnd: true,
+    useRedux: true,
+    useRouter: true,
+  });
+  await act(async () => {
+    jest.advanceTimersByTime(1000);
+  });
+
+  const clearBtn = screen.getByTestId(getTestId('clear-button'));
+  expect(clearBtn).not.toBeDisabled();
+  await act(async () => {
+    userEvent.click(clearBtn);
+  });
+  // Let the clear-all trigger round-trip through the filter plugin
+  await act(async () => {
+    jest.advanceTimersByTime(1000);
+  });
+
+  // The staged clear must survive the trigger completing: the default value
+  // must not be re-applied and Apply must stay enabled
+  expect(updateDataMaskSpy).not.toHaveBeenCalled();
+  expect(screen.queryByTitle('East')).not.toBeInTheDocument();
+  expect(screen.getByTestId(getTestId('apply-button'))).not.toBeDisabled();
+  updateDataMaskSpy.mockRestore();
+});
+
 test('FilterBar Clear All only clears in-scope filters, not out-of-scope 
ones', async () => {
   const inScopeFilterId = 'NATIVE_FILTER-in-scope';
   const outOfScopeRequiredFilterId = 'NATIVE_FILTER-out-of-scope-required';
@@ -831,7 +914,7 @@ test('FilterBar Clear All only clears in-scope filters, not 
out-of-scope ones',
 
   expect(updateDataMaskSpy).toHaveBeenCalledWith(inScopeFilterId, {
     id: inScopeFilterId,
-    filterState: { value: undefined, validateStatus: undefined },
+    filterState: { value: null, validateStatus: undefined },
     extraFormData: {},
   });
 
diff --git 
a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx 
b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx
index a08baa07a72..64b87bcdb82 100644
--- 
a/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx
+++ 
b/superset-frontend/src/dashboard/components/nativeFilters/FilterBar/index.tsx
@@ -517,22 +517,21 @@ const FilterBar: FC<FiltersBarProps> = ({
       // Only clear in-scope filters
       if (!inScopeFilterIds.has(id)) return;
 
-      // Range filters use [null, null] as the cleared value; others use 
undefined
-      const clearedValue =
-        filterType === 'filter_range' ? [null, null] : undefined;
+      // Cleared values stage as explicit null ([null, null] for ranges), never
+      // undefined: the select plugin's init effect treats undefined as
+      // "uninitialized" and would re-apply default values once the clear-all
+      // trigger completes.
+      const clearedValue = filterType === 'filter_range' ? [null, null] : null;
       const isRequired = !!filter.controlValues?.enableEmptyFilter;
       if (dataMaskSelected[id]) {
         // Stage the cleared value locally; do NOT dispatch to Redux here.
         // Persistence happens when the user clicks Apply.
         setDataMaskSelected(draft => {
-          if (draft[id].filterState?.value !== undefined) {
-            draft[id].filterState!.value = clearedValue;
-          }
           draft[id].extraFormData = {};
-          if (draft[id].filterState) {
-            draft[id].filterState!.validateStatus = isRequired
-              ? 'error'
-              : undefined;
+          const { filterState } = draft[id];
+          if (filterState) {
+            filterState.value = clearedValue;
+            filterState.validateStatus = isRequired ? 'error' : undefined;
           }
         });
         newClearAllTriggers[id] = true;
diff --git 
a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx 
b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx
index 5e137c4db35..94358bd222e 100644
--- a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx
+++ b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.test.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { AppSection } from '@superset-ui/core';
+import { AppSection, type ChartProps } from '@superset-ui/core';
 import { GenericDataType } from '@apache-superset/core/common';
 import userEvent from '@testing-library/user-event';
 import { render, screen } from 'spec/helpers/testing-library';
@@ -443,6 +443,35 @@ describe('RangeFilterPlugin', () => {
       );
     });
   });
+
+  test('clears a filter still sitting at its default value', () => {
+    const renderWith = (value: [number | null, number | null]) => (
+      <RangeFilterPlugin
+        {...(transformProps({
+          ...rangeProps,
+          filterState: { value },
+        } as unknown as ChartProps) as PluginFilterRangeProps)}
+        setDataMask={setDataMask}
+      />
+    );
+
+    // The filter loads at its default and the user never touches it.
+    const { rerender } = render(renderWith([10, 70]));
+    setDataMask.mockClear();
+
+    // Clear All stages [null, null] for range filters.
+    rerender(renderWith([null, null]));
+
+    expect(setDataMask).toHaveBeenCalledWith({
+      extraFormData: {},
+      filterState: {
+        value: [null, null],
+        label: '',
+        validateStatus: undefined,
+        validateMessage: '',
+      },
+    });
+  });
 });
 
 test('calculateStep returns ~100 steps for integer ranges', () => {
diff --git 
a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx 
b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx
index de275aeeb36..a414491d6bc 100644
--- a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx
+++ b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx
@@ -363,8 +363,14 @@ export default function RangeFilterPlugin(props: 
PluginFilterRangeProps) {
       return;
     }
 
-    // Clear all case
-    if (filterState.value === undefined && !filterState.validateStatus) {
+    // Clear all case. The filter bar stages [null, null] for range filters, so
+    // matching only undefined let a filter still sitting at its default fall
+    // through to the default-restoring branch below.
+    if (
+      (filterState.value === undefined ||
+        isEqualArray(filterState.value, [null, null])) &&
+      !filterState.validateStatus
+    ) {
       setInputValue([null, null]);
       updateDataMaskValue([null, null]);
       return;

Reply via email to