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;