From b90c6bd6c8a2bcd4549a2ae911353d195ceab350 Mon Sep 17 00:00:00 2001 From: Superset Dev Date: Wed, 16 Sep 2026 16:45:16 -0700 Subject: [PATCH 1/5] fix(native-filters): keep cascade dependency gate in sync with live filter type Follow-up to #43223. Two bot review comments on that PR were never addressed before merge: - FiltersConfigForm.tsx: canDependOnOtherFilters read formFilter?.filterType directly, unlike itemTypeField's own fallback chain (formFilter?.filterType || filterToEdit?.filterType || 'filter_select') used everywhere else in this component. formFilter?.filterType can be undefined on the first render before the antd Form hydrates, which hid the "Values are dependent on other filters" section even for filter types that do support cascading. - useFilterOperations.ts: buildDependencyMap read each filter's dependencies array as-is, without re-checking that a listed parent still supports cascading. If a parent's type changes to one that no longer supports dependencies within the same open-modal editing session, the stale relationship lingered in the live dependency map and preview until save. Now filters each parent id through the existing canBeUsedAsDependency check on every rebuild. Added regression tests for the buildDependencyMap fix (confirmed they fail against the pre-fix code). Did not add a FiltersConfigForm.tsx-level test for the itemTypeField fix - no existing test harness covers this component at that level (matches #43223's own testing approach, which relied on manual QA there and unit tests only for the hooks). Co-Authored-By: Evan Rusackas Co-Authored-By: Claude Sonnet 5 --- .../FiltersConfigForm/FiltersConfigForm.tsx | 8 ++-- .../hooks/useFilterOperations.test.ts | 45 +++++++++++++++++++ .../hooks/useFilterOperations.ts | 10 ++++- 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx index ce90acef857b..ae8ecba4d5f7 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx @@ -477,9 +477,11 @@ const FiltersConfigForm = ( formFilter?.filterType, ); - const canDependOnOtherFilters = filterSupportsDependencies( - formFilter?.filterType, - ); + // Use itemTypeField, not formFilter?.filterType directly: the latter can + // be undefined on the first render before the antd Form hydrates (see + // itemTypeField's own fallback chain above), which would otherwise hide + // this section for a filter type that does support cascade dependencies. + const canDependOnOtherFilters = filterSupportsDependencies(itemTypeField); const isDataDirty = formFilter?.isDataDirty ?? true; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/hooks/useFilterOperations.test.ts b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/hooks/useFilterOperations.test.ts index 87a6d475341f..cee75ea294a5 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/hooks/useFilterOperations.test.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/hooks/useFilterOperations.test.ts @@ -23,6 +23,7 @@ import type { FormInstance } from '@superset-ui/core/components'; import { filterSupportsDependencies, useFilterOperations, + FilterOperationsParams, } from './useFilterOperations'; import { useItemStateManager } from './useItemStateManager'; import { NativeFiltersForm } from '../types'; @@ -256,3 +257,47 @@ test('restoreFilter cancels the pending removal before the delay elapses', () => jest.useRealTimers(); }); + +function renderFilterOperations( + filters: Record, +) { + const params: FilterOperationsParams = { + form: { + getFieldValue: () => filters, + } as unknown as FilterOperationsParams['form'], + filterState: { + removedItems: {}, + } as unknown as FilterOperationsParams['filterState'], + filterIds: Object.keys(filters), + filterConfigMap: {}, + handleModifyItem: jest.fn(), + setActiveItem: jest.fn(), + setSaveAlertVisible: jest.fn(), + }; + return renderHook(() => useFilterOperations(params)).result; +} + +test('buildDependencyMap drops a parent id whose filter type no longer supports dependencies', () => { + // "parent" was a Select filter when "child" was configured to depend on + // it, then the user changed "parent" to Time grain within the same + // editing session (before saving) - the stale edge should not linger. + const result = renderFilterOperations({ + parent: { filterType: 'filter_timegrain' }, + child: { filterType: 'filter_select', dependencies: ['parent'] }, + }); + + const dependencyMap = result.current.buildDependencyMap(); + + expect(dependencyMap.get('child')).toEqual([]); +}); + +test('buildDependencyMap keeps a parent id whose filter type still supports dependencies', () => { + const result = renderFilterOperations({ + parent: { filterType: 'filter_select' }, + child: { filterType: 'filter_select', dependencies: ['parent'] }, + }); + + const dependencyMap = result.current.buildDependencyMap(); + + expect(dependencyMap.get('child')).toEqual(['parent']); +}); diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/hooks/useFilterOperations.ts b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/hooks/useFilterOperations.ts index f889b86ab750..940c22e68488 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/hooks/useFilterOperations.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/hooks/useFilterOperations.ts @@ -189,11 +189,17 @@ export function useFilterOperations({ } else if (configItem?.cascadeParentIds) { array = [...configItem.cascadeParentIds]; } - dependencyMap.set(key, array); + // Drop parent ids that no longer qualify (removed, or its filter + // type changed to one that doesn't support cascade dependencies) + // as soon as the map is rebuilt, instead of only at save time. + dependencyMap.set( + key, + array.filter(parentId => canBeUsedAsDependency(parentId)), + ); }); } return dependencyMap; - }, [filterConfigMap, form]); + }, [canBeUsedAsDependency, filterConfigMap, form]); const getAvailableFilters = useCallback( (filterId: string, getItemTitle: (id: string) => string) => { From daf44a6e17810766122d94ab014bd9fa12c588ca Mon Sep 17 00:00:00 2001 From: rusackas Date: Sat, 19 Sep 2026 14:57:37 -0700 Subject: [PATCH 2/5] test(native-filters): cover first-render dependency control for saved cascade filters Adds a regression test asserting the "Values are dependent on other filters" control renders immediately when opening a saved Select filter that already has a cascade parent, without requiring a rerender. Co-Authored-By: Evan Rusackas Co-Authored-By: Claude Sonnet 5 --- .../FiltersConfigModal.test.tsx | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx index 750dc1948fad..150996d7d6d2 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx @@ -532,6 +532,28 @@ test('deletes a filter including dependencies', async () => { ); }, 30000); +test('shows the dependency control on first render for a saved cascade filter', () => { + const nativeFilterConfig = [ + buildNativeFilter('NATIVE_FILTER-1', 'state', ['NATIVE_FILTER-2']), + buildNativeFilter('NATIVE_FILTER-2', 'country', []), + ]; + const state = { + ...defaultState(), + dashboardInfo: { + metadata: { + native_filter_configuration: nativeFilterConfig, + }, + }, + dashboardLayout, + }; + defaultRender(state, { ...props, createNewOnOpen: false }); + + // No interaction: the dependency control and its saved parent must be + // visible as soon as the modal opens on a filter that already has a + // cascade parent, without waiting for a rerender. + expect(getCheckbox(DEPENDENCIES_REGEX)).toBeChecked(); +}); + const SORTABLE_ITEM_HEIGHT = 40; const SORTABLE_ITEM_WIDTH = 200; From 0623ec72c3296913f5bf2ff2ff5ac839783bc501 Mon Sep 17 00:00:00 2001 From: rusackas Date: Sat, 19 Sep 2026 18:27:51 -0700 Subject: [PATCH 3/5] test(native-filters): fix accessible-name regex for dependency checkbox DEPENDENCIES_REGEX anchored on `$`, but the checkbox's accessible name includes the trailing tooltip icon text ("... other filters info-circle"), so it never matched and the new first-render regression test failed in CI. Drop the trailing anchor to match the pattern already used by the other tooltip-bearing checkbox regexes in this file. Co-Authored-By: Evan Rusackas Co-Authored-By: Claude Sonnet 5 --- .../FiltersConfigModal/FiltersConfigModal.test.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx index 150996d7d6d2..bfb843729d46 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx @@ -173,7 +173,10 @@ const FILTER_SETTINGS_REGEX = /^filter settings$/i; const DEFAULT_VALUE_REGEX = /^filter has default value$/i; const MULTIPLE_REGEX = /^can select multiple values$/i; const FILTER_REQUIRED_REGEX = /^filter value is required/i; -const DEPENDENCIES_REGEX = /^values are dependent on other filters$/i; +// No trailing `$`: like the other tooltip-bearing checkboxes below, the +// accessible name includes the trailing info icon (e.g. "... other filters +// info-circle"), so an exact-end anchor would never match. +const DEPENDENCIES_REGEX = /^values are dependent on other filters/i; const FIRST_VALUE_REGEX = /^select first filter value by default/i; const INVERSE_SELECTION_REGEX = /^inverse selection/i; const SEARCH_ALL_REGEX = /^dynamically search all filter values/i; From b379d3e4c87d9d77b1ccd04466605758d4be343a Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Tue, 29 Sep 2026 02:18:06 -0700 Subject: [PATCH 4/5] fix(native-filters): fix hasAdditionalFilters first-render read + strengthen dependency test hasAdditionalFilters read formFilter?.filterType directly, which is undefined on the first render before the antd Form hydrates -- the same bug already fixed for canDependOnOtherFilters just below it. This hid the pre-filter section on first paint for a saved cascade filter. Also strengthens the first-render regression test to assert the saved parent actually renders (not a placeholder) and that the pre-filter control is present on first paint too. Co-Authored-By: Evan Rusackas Co-Authored-By: Claude Sonnet 5 --- .../FiltersConfigForm/FiltersConfigForm.tsx | 10 ++++----- .../FiltersConfigModal.test.tsx | 21 ++++++++++++++++--- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx index ae8ecba4d5f7..ee3f745dbccc 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx @@ -473,14 +473,14 @@ const FiltersConfigForm = ( const hasFilledDataset = !hasDataset || (datasetId && (formFilter?.column || !hasColumn)); - const hasAdditionalFilters = FILTERS_WITH_ADHOC_FILTERS.includes( - formFilter?.filterType, - ); - // Use itemTypeField, not formFilter?.filterType directly: the latter can // be undefined on the first render before the antd Form hydrates (see // itemTypeField's own fallback chain above), which would otherwise hide - // this section for a filter type that does support cascade dependencies. + // this section and the cascade-dependency section for a filter type that + // does support them. + const hasAdditionalFilters = + FILTERS_WITH_ADHOC_FILTERS.includes(itemTypeField); + const canDependOnOtherFilters = filterSupportsDependencies(itemTypeField); const isDataDirty = formFilter?.isDataDirty ?? true; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx index bfb843729d46..a8396bb20bd5 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx @@ -551,10 +551,25 @@ test('shows the dependency control on first render for a saved cascade filter', }; defaultRender(state, { ...props, createNewOnOpen: false }); - // No interaction: the dependency control and its saved parent must be - // visible as soon as the modal opens on a filter that already has a - // cascade parent, without waiting for a rerender. + // No interaction: the dependency control must be checked as soon as the + // modal opens on a filter that already has a cascade parent, without + // waiting for a rerender. expect(getCheckbox(DEPENDENCIES_REGEX)).toBeChecked(); + + // The saved parent ("country") must render as the actual selected + // dependency, not a "(deleted or invalid type)" placeholder. antd Select + // renders the active selection as a span whose title attribute is the + // picked option's label. + expect( + document.querySelector( + '.ant-select-content-has-value[title="country"], .ant-select-selection-item[title="country"]', + ), + ).toBeInTheDocument(); + + // hasAdditionalFilters has the same first-render read as + // canDependOnOtherFilters above: the pre-filter control must also be + // present (not merely unchecked) on the very first paint. + expect(getCheckbox(PRE_FILTER_REGEX)).not.toBeChecked(); }); const SORTABLE_ITEM_HEIGHT = 40; From 0955120bd963df0beb818da67017b5765655afae Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Tue, 29 Sep 2026 20:30:09 -0700 Subject: [PATCH 5/5] test(native-filters): cover buildDependencyMap for a pending-removal parent sadpandajoe flagged that the removedItems-aware canBeUsedAsDependency check has no test where a parent is pending removal; add a case asserting the child's dependency map drops it. Co-Authored-By: Evan Rusackas Co-Authored-By: Claude Sonnet 5 --- .../hooks/useFilterOperations.test.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/hooks/useFilterOperations.test.ts b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/hooks/useFilterOperations.test.ts index cee75ea294a5..d4f323b9560d 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/hooks/useFilterOperations.test.ts +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/hooks/useFilterOperations.test.ts @@ -260,13 +260,14 @@ test('restoreFilter cancels the pending removal before the delay elapses', () => function renderFilterOperations( filters: Record, + removedItems: Record = {}, ) { const params: FilterOperationsParams = { form: { getFieldValue: () => filters, } as unknown as FilterOperationsParams['form'], filterState: { - removedItems: {}, + removedItems, } as unknown as FilterOperationsParams['filterState'], filterIds: Object.keys(filters), filterConfigMap: {}, @@ -301,3 +302,19 @@ test('buildDependencyMap keeps a parent id whose filter type still supports depe expect(dependencyMap.get('child')).toEqual(['parent']); }); + +test('buildDependencyMap drops a parent id that is pending removal', () => { + // "parent" is queued for removal but the form still lists it as + // "child"'s dependency until the pending delete is confirmed or undone. + const result = renderFilterOperations( + { + parent: { filterType: 'filter_select' }, + child: { filterType: 'filter_select', dependencies: ['parent'] }, + }, + { parent: { isPending: true } }, + ); + + const dependencyMap = result.current.buildDependencyMap(); + + expect(dependencyMap.get('child')).toEqual([]); +});