mirror of
https://github.com/apache/superset.git
synced 2026-05-07 08:54:23 +00:00
fix(time-range-modal): time range modal for out of scope filter is not displayed correctly (#36996)
This commit is contained in:
committed by
GitHub
parent
04c5517206
commit
54919c942a
@@ -17,6 +17,10 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { render, screen } from 'spec/helpers/testing-library';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Store } from 'redux';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import { stateWithoutNativeFilters } from 'spec/fixtures/mockStore';
|
||||
import {
|
||||
ChartCustomizationType,
|
||||
@@ -25,7 +29,9 @@ import {
|
||||
import { FilterBarOrientation } from 'src/dashboard/types';
|
||||
import FilterControls from './FilterControls';
|
||||
|
||||
const mockStore = {
|
||||
const mockStore = configureStore([thunk]);
|
||||
|
||||
const mockStoreForCustomization = {
|
||||
...stateWithoutNativeFilters,
|
||||
dashboardInfo: {
|
||||
...stateWithoutNativeFilters.dashboardInfo,
|
||||
@@ -35,7 +41,7 @@ const mockStore = {
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useSelector: jest.fn(selector => selector(mockStore)),
|
||||
useSelector: jest.fn(selector => selector(mockStoreForCustomization)),
|
||||
useDispatch: () => jest.fn(),
|
||||
}));
|
||||
|
||||
@@ -96,9 +102,9 @@ test('renders multiple chart customization dividers in vertical mode', () => {
|
||||
|
||||
test('renders chart customization divider in horizontal mode', () => {
|
||||
const horizontalStore = {
|
||||
...mockStore,
|
||||
...mockStoreForCustomization,
|
||||
dashboardInfo: {
|
||||
...mockStore.dashboardInfo,
|
||||
...mockStoreForCustomization.dashboardInfo,
|
||||
filterBarOrientation: FilterBarOrientation.Horizontal,
|
||||
},
|
||||
};
|
||||
@@ -184,3 +190,222 @@ test('renders empty state when no chart customizations provided', () => {
|
||||
container.querySelector('.chart-customization-item-wrapper'),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const createMockFilter = (id: string, name: string) => ({
|
||||
id,
|
||||
name,
|
||||
filterType: 'filter_select',
|
||||
targets: [{ datasetId: 1, column: { name: 'country' } }],
|
||||
defaultDataMask: {},
|
||||
controlValues: {},
|
||||
cascadeParentIds: [],
|
||||
scope: {
|
||||
rootPath: ['ROOT_ID'],
|
||||
excluded: [] as string[],
|
||||
},
|
||||
isInstant: true,
|
||||
allowsMultipleValues: true,
|
||||
isRequired: false,
|
||||
});
|
||||
|
||||
const getDefaultState = (orientation: FilterBarOrientation) => ({
|
||||
dashboardInfo: {
|
||||
id: 1,
|
||||
filterBarOrientation: orientation,
|
||||
},
|
||||
dashboardLayout: {
|
||||
present: {
|
||||
ROOT_ID: {
|
||||
type: 'ROOT',
|
||||
id: 'ROOT_ID',
|
||||
children: ['TABS-1'],
|
||||
},
|
||||
'TABS-1': {
|
||||
type: 'TABS',
|
||||
id: 'TABS-1',
|
||||
children: ['TAB-1', 'TAB-2'],
|
||||
},
|
||||
'TAB-1': {
|
||||
type: 'TAB',
|
||||
id: 'TAB-1',
|
||||
children: ['CHART-1'],
|
||||
},
|
||||
'TAB-2': {
|
||||
type: 'TAB',
|
||||
id: 'TAB-2',
|
||||
children: ['CHART-2'],
|
||||
},
|
||||
'CHART-1': {
|
||||
type: 'CHART',
|
||||
id: 'CHART-1',
|
||||
meta: { chartId: 1 },
|
||||
},
|
||||
'CHART-2': {
|
||||
type: 'CHART',
|
||||
id: 'CHART-2',
|
||||
meta: { chartId: 2 },
|
||||
},
|
||||
},
|
||||
},
|
||||
charts: {
|
||||
1: { id: 1, formData: {} },
|
||||
2: { id: 2, formData: {} },
|
||||
},
|
||||
dataMask: {},
|
||||
nativeFilters: {
|
||||
filters: {
|
||||
'filter-1': createMockFilter('filter-1', 'Country Filter'),
|
||||
'filter-2': createMockFilter('filter-2', 'Region Filter'),
|
||||
'filter-3': createMockFilter('filter-3', 'City Filter'),
|
||||
},
|
||||
filterSets: {},
|
||||
},
|
||||
dashboardState: {
|
||||
directPathToChild: [],
|
||||
activeTabs: ['TAB-1'],
|
||||
chartCustomizationItems: [],
|
||||
},
|
||||
sliceEntities: {
|
||||
slices: {
|
||||
1: {
|
||||
slice_id: 1,
|
||||
slice_name: 'Chart 1',
|
||||
form_data: {},
|
||||
},
|
||||
2: {
|
||||
slice_id: 2,
|
||||
slice_name: 'Chart 2',
|
||||
form_data: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
datasources: {},
|
||||
});
|
||||
|
||||
function setupWithFilters(overrideState: any = {}, props: any = {}) {
|
||||
const state = {
|
||||
...getDefaultState(FilterBarOrientation.Vertical),
|
||||
...overrideState,
|
||||
};
|
||||
const store = mockStore(state) as Store;
|
||||
|
||||
return render(
|
||||
<Provider store={store}>
|
||||
<FilterControls
|
||||
dataMaskSelected={{}}
|
||||
onFilterSelectionChange={jest.fn()}
|
||||
onPendingCustomizationDataMaskChange={jest.fn()}
|
||||
chartCustomizationValues={[]}
|
||||
{...props}
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
}
|
||||
|
||||
test('FilterControls should mark out-of-scope filters as not overflowed in vertical mode', () => {
|
||||
const stateWithVertical = getDefaultState(FilterBarOrientation.Vertical);
|
||||
|
||||
stateWithVertical.nativeFilters.filters['filter-3'].scope = {
|
||||
rootPath: ['ROOT_ID'],
|
||||
excluded: ['TAB-1'],
|
||||
};
|
||||
|
||||
const { container } = setupWithFilters(stateWithVertical);
|
||||
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('FilterControls should mark out-of-scope filters as overflowed in horizontal mode', () => {
|
||||
const stateWithHorizontal = getDefaultState(FilterBarOrientation.Horizontal);
|
||||
|
||||
stateWithHorizontal.nativeFilters.filters['filter-3'].scope = {
|
||||
rootPath: ['ROOT_ID'],
|
||||
excluded: ['TAB-1'],
|
||||
};
|
||||
|
||||
const { container } = setupWithFilters(stateWithHorizontal);
|
||||
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('FilterControls overflowedByIndex calculation respects filter bar orientation', () => {
|
||||
const verticalState = getDefaultState(FilterBarOrientation.Vertical);
|
||||
verticalState.nativeFilters.filters['filter-2'].scope = {
|
||||
rootPath: ['ROOT_ID'],
|
||||
excluded: ['TAB-1'],
|
||||
};
|
||||
|
||||
const { rerender, container } = setupWithFilters(verticalState);
|
||||
expect(container).toBeInTheDocument();
|
||||
|
||||
const horizontalState = getDefaultState(FilterBarOrientation.Horizontal);
|
||||
horizontalState.nativeFilters.filters['filter-2'].scope = {
|
||||
rootPath: ['ROOT_ID'],
|
||||
excluded: ['TAB-1'],
|
||||
};
|
||||
|
||||
rerender(
|
||||
<Provider store={mockStore(horizontalState) as Store}>
|
||||
<FilterControls
|
||||
dataMaskSelected={{}}
|
||||
onFilterSelectionChange={jest.fn()}
|
||||
onPendingCustomizationDataMaskChange={jest.fn()}
|
||||
chartCustomizationValues={[]}
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('FilterControls should correctly pass isOverflowing prop to filter controls', () => {
|
||||
const state = getDefaultState(FilterBarOrientation.Vertical);
|
||||
|
||||
state.nativeFilters.filters['filter-1'].scope = {
|
||||
rootPath: ['ROOT_ID'],
|
||||
excluded: [],
|
||||
};
|
||||
|
||||
state.nativeFilters.filters['filter-2'].scope = {
|
||||
rootPath: ['ROOT_ID'],
|
||||
excluded: ['TAB-1'],
|
||||
};
|
||||
|
||||
const { container } = setupWithFilters(state);
|
||||
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('FilterControls should handle empty filters list', () => {
|
||||
const state = getDefaultState(FilterBarOrientation.Vertical);
|
||||
state.nativeFilters.filters = {} as any;
|
||||
|
||||
const { container } = setupWithFilters(state);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('FilterControls overflowedByIndex updates when filters change scope', () => {
|
||||
const state = getDefaultState(FilterBarOrientation.Vertical);
|
||||
|
||||
const { container, rerender } = setupWithFilters(state);
|
||||
expect(container).toBeInTheDocument();
|
||||
|
||||
const updatedState = getDefaultState(FilterBarOrientation.Vertical);
|
||||
updatedState.nativeFilters.filters['filter-1'].scope = {
|
||||
rootPath: ['ROOT_ID'],
|
||||
excluded: ['TAB-1'],
|
||||
};
|
||||
|
||||
rerender(
|
||||
<Provider store={mockStore(updatedState) as Store}>
|
||||
<FilterControls
|
||||
dataMaskSelected={{}}
|
||||
onFilterSelectionChange={jest.fn()}
|
||||
onPendingCustomizationDataMaskChange={jest.fn()}
|
||||
chartCustomizationValues={[]}
|
||||
/>
|
||||
</Provider>,
|
||||
);
|
||||
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -659,12 +659,19 @@ const FilterControls: FC<FilterControlsProps> = ({
|
||||
overflowedFiltersInScope.map(({ id }) => id),
|
||||
);
|
||||
|
||||
return filtersWithValues.map(
|
||||
filter =>
|
||||
filtersOutOfScopeIds.has(filter.id) ||
|
||||
overflowedFiltersInScopeIds.has(filter.id),
|
||||
);
|
||||
}, [filtersOutOfScope, filtersWithValues, overflowedFiltersInScope]);
|
||||
return filtersWithValues.map(filter => {
|
||||
// Out-of-scope filters in vertical mode are in a Collapse panel, not overflowed
|
||||
if (filtersOutOfScopeIds.has(filter.id)) {
|
||||
return filterBarOrientation === FilterBarOrientation.Horizontal;
|
||||
}
|
||||
return overflowedFiltersInScopeIds.has(filter.id);
|
||||
});
|
||||
}, [
|
||||
filtersOutOfScope,
|
||||
filtersWithValues,
|
||||
overflowedFiltersInScope,
|
||||
filterBarOrientation,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (outlinedFilterId && overflowedIds.includes(outlinedFilterId)) {
|
||||
|
||||
@@ -93,3 +93,46 @@ test('Open and close popover', () => {
|
||||
expect(defaultProps.onClosePopover).toHaveBeenCalled();
|
||||
expect(screen.queryByText('Edit time range')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('DateFilter popover should attach to document.body when not overflowing', () => {
|
||||
render(setup({ ...defaultProps, isOverflowingFilterBar: false }));
|
||||
|
||||
userEvent.click(screen.getByText(NO_TIME_RANGE));
|
||||
|
||||
const popover = document.querySelector('.time-range-popover');
|
||||
expect(popover?.parentElement).toBe(document.body);
|
||||
});
|
||||
|
||||
test('DateFilter popover should attach to parent node when overflowing in filter bar', () => {
|
||||
render(setup({ ...defaultProps, isOverflowingFilterBar: true }));
|
||||
|
||||
userEvent.click(screen.getByText(NO_TIME_RANGE));
|
||||
|
||||
const popover = document.querySelector('.time-range-popover');
|
||||
const trigger = screen.getByTestId(DateFilterTestKey.PopoverOverlay);
|
||||
|
||||
expect(popover?.parentElement).toBe(trigger.parentElement);
|
||||
});
|
||||
|
||||
test('DateFilter should properly handle isOverflowingFilterBar prop changes', () => {
|
||||
const { rerender } = render(
|
||||
setup({ ...defaultProps, isOverflowingFilterBar: false }),
|
||||
);
|
||||
|
||||
// When not overflowing, popover should attach to document.body
|
||||
userEvent.click(screen.getByText(NO_TIME_RANGE));
|
||||
const popover = document.querySelector('.time-range-popover');
|
||||
expect(popover?.parentElement).toBe(document.body);
|
||||
|
||||
userEvent.click(screen.getByText('CANCEL'));
|
||||
|
||||
// When overflowing, popover should attach to parent node
|
||||
rerender(setup({ ...defaultProps, isOverflowingFilterBar: true }));
|
||||
userEvent.click(screen.getByText(NO_TIME_RANGE));
|
||||
|
||||
const popoverAfterRerender = document.querySelector('.time-range-popover');
|
||||
const trigger = screen.getByTestId(DateFilterTestKey.PopoverOverlay);
|
||||
|
||||
expect(popoverAfterRerender?.parentElement).toBe(trigger.parentElement);
|
||||
expect(popoverAfterRerender?.parentElement).not.toBe(document.body);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user