mirror of
https://github.com/apache/superset.git
synced 2026-07-19 21:25:38 +00:00
fix(dashboard): normalize legacy currentState to filterState in native_filters URL param (#40929)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,7 @@ import {
|
||||
UPDATE_COMPONENTS,
|
||||
} from 'src/dashboard/actions/dashboardLayout';
|
||||
import { DASHBOARD_HEADER_ID } from 'src/dashboard/util/constants';
|
||||
import { getUrlParam } from 'src/utils/urlUtils';
|
||||
import DashboardPage from './DashboardPage';
|
||||
|
||||
const mockTheme = {
|
||||
@@ -133,9 +134,11 @@ jest.mock('src/dashboard/util/activeDashboardFilters', () => ({
|
||||
}));
|
||||
|
||||
jest.mock('src/utils/urlUtils', () => ({
|
||||
getUrlParam: () => null,
|
||||
getUrlParam: jest.fn().mockReturnValue(null),
|
||||
}));
|
||||
|
||||
const mockGetUrlParam = getUrlParam as jest.Mock;
|
||||
|
||||
jest.mock('src/dashboard/components/nativeFilters/FilterBar/keyValue', () => ({
|
||||
getFilterValue: jest.fn(),
|
||||
getPermalinkValue: jest.fn(),
|
||||
@@ -161,6 +164,10 @@ beforeEach(() => {
|
||||
// Tests assert against the global document.title and the unmount restore
|
||||
// effect can carry title state across tests, so reset it for isolation.
|
||||
document.title = '';
|
||||
// clearAllMocks does not reset mockImplementation — reset explicitly so
|
||||
// per-test overrides don't leak into subsequent tests.
|
||||
mockGetUrlParam.mockReset();
|
||||
mockGetUrlParam.mockReturnValue(null);
|
||||
mockUseDashboard.mockReturnValue({
|
||||
result: mockDashboard,
|
||||
error: null,
|
||||
@@ -443,6 +450,115 @@ test('passes null theme when Redux dashboardInfo.theme is explicitly null (theme
|
||||
);
|
||||
});
|
||||
|
||||
test('copies currentState to filterState for legacy native_filters URL params', async () => {
|
||||
// Pre-2021 URLs encode filter selections under `currentState`. The dataMask
|
||||
// reducer uses `filterState`, so without normalization the filter panel shows
|
||||
// no active selections even though extraFormData still filters chart queries.
|
||||
mockGetUrlParam.mockImplementation((param: { name: string }) => {
|
||||
if (param.name === 'native_filters') {
|
||||
return {
|
||||
'NATIVE_FILTER-OvPTDNKc9': {
|
||||
extraFormData: {
|
||||
filters: [{ col: 'team_name', op: 'IN', val: ['MarginEdge'] }],
|
||||
},
|
||||
currentState: { value: ['MarginEdge'] },
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
render(
|
||||
<Suspense fallback="loading">
|
||||
<DashboardPage idOrSlug="1" />
|
||||
</Suspense>,
|
||||
{
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
initialState: {
|
||||
dashboardInfo: { id: 1, metadata: {} },
|
||||
dashboardState: { sliceIds: [] },
|
||||
nativeFilters: { filters: {} },
|
||||
dataMask: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('loading')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(hydrateDashboard).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dataMask: expect.objectContaining({
|
||||
'NATIVE_FILTER-OvPTDNKc9': expect.objectContaining({
|
||||
filterState: { value: ['MarginEdge'] },
|
||||
extraFormData: {
|
||||
filters: [{ col: 'team_name', op: 'IN', val: ['MarginEdge'] }],
|
||||
},
|
||||
currentState: { value: ['MarginEdge'] },
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('does not overwrite filterState when modern native_filters URL format is used', async () => {
|
||||
// Modern URLs already carry `filterState`; the normalization must not clobber it.
|
||||
mockGetUrlParam.mockImplementation((param: { name: string }) => {
|
||||
if (param.name === 'native_filters') {
|
||||
return {
|
||||
'NATIVE_FILTER-OvPTDNKc9': {
|
||||
extraFormData: {
|
||||
filters: [{ col: 'team_name', op: 'IN', val: ['MarginEdge'] }],
|
||||
},
|
||||
filterState: { value: ['MarginEdge'] },
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
render(
|
||||
<Suspense fallback="loading">
|
||||
<DashboardPage idOrSlug="1" />
|
||||
</Suspense>,
|
||||
{
|
||||
useRedux: true,
|
||||
useRouter: true,
|
||||
initialState: {
|
||||
dashboardInfo: { id: 1, metadata: {} },
|
||||
dashboardState: { sliceIds: [] },
|
||||
nativeFilters: { filters: {} },
|
||||
dataMask: {},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('loading')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(hydrateDashboard).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dataMask: expect.objectContaining({
|
||||
'NATIVE_FILTER-OvPTDNKc9': expect.objectContaining({
|
||||
filterState: { value: ['MarginEdge'] },
|
||||
extraFormData: {
|
||||
filters: [{ col: 'team_name', op: 'IN', val: ['MarginEdge'] }],
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
// currentState must not have been injected
|
||||
const callArg = (hydrateDashboard as jest.Mock).mock.calls[0][0];
|
||||
expect(
|
||||
callArg.dataMask['NATIVE_FILTER-OvPTDNKc9'].currentState,
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
test('clears undo history after hydrating the dashboard', async () => {
|
||||
render(
|
||||
<Suspense fallback="loading">
|
||||
|
||||
@@ -223,7 +223,22 @@ export const DashboardPage: FC<PageProps> = ({ idOrSlug }: PageProps) => {
|
||||
dataMask = await getFilterValue(id, nativeFilterKeyValue);
|
||||
}
|
||||
if (isOldRison) {
|
||||
dataMask = isOldRison;
|
||||
// Normalize legacy `currentState` → `filterState`. Pre-2021 URLs stored
|
||||
// per-filter selections under `currentState`; modern dataMask uses
|
||||
// `filterState`. Without this copy the filter panel shows no active
|
||||
// selections even though extraFormData still applies the query filter.
|
||||
if (typeof isOldRison === 'object' && isOldRison !== null) {
|
||||
dataMask = Object.fromEntries(
|
||||
Object.entries(
|
||||
isOldRison as Record<string, Record<string, unknown>>,
|
||||
).map(([filterId, entry]) => [
|
||||
filterId,
|
||||
entry?.currentState && !entry?.filterState
|
||||
? { ...entry, filterState: entry.currentState }
|
||||
: entry,
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Rison URL filters with intelligent native filter injection
|
||||
|
||||
Reference in New Issue
Block a user