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 d203f0de337 chore(sql-lab): finish SqlLab typed-dispatch migration for 
SaveDatasetModal (#40040)
d203f0de337 is described below

commit d203f0de3375fd1ddb962d0596395e6d12488827
Author: Evan Rusackas <[email protected]>
AuthorDate: Wed May 20 16:04:38 2026 -0700

    chore(sql-lab): finish SqlLab typed-dispatch migration for SaveDatasetModal 
(#40040)
    
    Co-authored-by: Claude Code <[email protected]>
    Co-authored-by: Claude <[email protected]>
---
 superset-frontend/src/SqlLab/actions/sqlLab.ts     |   7 +-
 .../SaveDatasetModal/SaveDatasetModal.test.tsx     | 105 ++++++++-------------
 .../SqlLab/components/SaveDatasetModal/index.tsx   |  11 +--
 3 files changed, 46 insertions(+), 77 deletions(-)

diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.ts 
b/superset-frontend/src/SqlLab/actions/sqlLab.ts
index 302f0b82cb0..7058471a55b 100644
--- a/superset-frontend/src/SqlLab/actions/sqlLab.ts
+++ b/superset-frontend/src/SqlLab/actions/sqlLab.ts
@@ -1671,7 +1671,7 @@ export interface VizOptions {
 
 export function createDatasource(
   vizOptions: VizOptions,
-): SqlLabThunkAction<Promise<unknown>> {
+): SqlLabThunkAction<Promise<{ id: number }>> {
   return (dispatch: AppDispatch) => {
     dispatch(createDatasourceStarted());
     const { dbId, catalog, schema, datasourceName, sql, templateParams } =
@@ -1691,9 +1691,10 @@ export function createDatasource(
       }),
     })
       .then(({ json }) => {
-        dispatch(createDatasourceSuccess(json as { id: number }));
+        const result = json as { id: number };
+        dispatch(createDatasourceSuccess(result));
 
-        return Promise.resolve(json);
+        return result;
       })
       .catch(error => {
         getClientErrorObject(error).then(e => {
diff --git 
a/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
 
b/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
index b89f1ef9525..861a4772858 100644
--- 
a/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
+++ 
b/superset-frontend/src/SqlLab/components/SaveDatasetModal/SaveDatasetModal.test.tsx
@@ -16,8 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import * as reactRedux from 'react-redux';
-import { act } from 'react';
+import { act, type ComponentProps } from 'react';
 import {
   cleanup,
   fireEvent,
@@ -40,6 +39,19 @@ const mockedProps = {
   datasource: testQuery,
 };
 
+// Render with the SqlLab user fixture preloaded into the mock store so the
+// component's useSelector(state => state.user) returns a useful value.
+// Previously this test used jest.spyOn(reactRedux, 'useSelector') to inject
+// the user directly, which can't intercept calls routed through the typed
+// useAppSelector hook.
+const renderModal = (
+  props: Partial<ComponentProps<typeof SaveDatasetModal>> = {},
+) =>
+  render(<SaveDatasetModal {...mockedProps} {...props} />, {
+    useRedux: true,
+    initialState: { user },
+  });
+
 fetchMock.get('glob:*/api/v1/dataset/?*', {
   result: mockdatasets,
   dataset_count: 3,
@@ -47,17 +59,17 @@ fetchMock.get('glob:*/api/v1/dataset/?*', {
 
 jest.useFakeTimers({ advanceTimers: true });
 
-// Mock the user
-const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
 beforeEach(() => {
-  useSelectorMock.mockClear();
   cleanup();
 });
 
-// Mock the createDatasource action
-const useDispatchMock = jest.spyOn(reactRedux, 'useDispatch');
+// Mock createDatasource to return a thunk that resolves with the dataset's
+// new id. The test's mock store includes redux-thunk middleware (from RTK's
+// getDefaultMiddleware), so dispatch(createDatasource(...)) properly unwraps
+// the thunk and the production code's .then((data) => 
clearDatasetCache(data.id))
+// chain receives `{ id: 123 }`. Individual tests can override per-call as 
needed.
 jest.mock('src/SqlLab/actions/sqlLab', () => ({
-  createDatasource: jest.fn(),
+  createDatasource: jest.fn(() => () => Promise.resolve({ id: 123 })),
 }));
 jest.mock('src/explore/exploreUtils/formData', () => ({
   postFormData: jest.fn(),
@@ -70,7 +82,7 @@ jest.mock('src/utils/cachedSupersetGet', () => ({
 // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from 
describe blocks
 describe('SaveDatasetModal', () => {
   test('renders a "Save as new" field', () => {
-    render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
+    renderModal();
 
     const saveRadioBtn = screen.getByRole('radio', {
       name: /save as new/i,
@@ -87,7 +99,7 @@ describe('SaveDatasetModal', () => {
   });
 
   test('renders an "Overwrite existing" field', () => {
-    render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
+    renderModal();
 
     const overwriteRadioBtn = screen.getByRole('radio', {
       name: /overwrite existing/i,
@@ -103,20 +115,20 @@ describe('SaveDatasetModal', () => {
   });
 
   test('renders a close button', () => {
-    render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
+    renderModal();
 
     expect(screen.getByRole('button', { name: /close/i })).toBeInTheDocument();
   });
 
   test('renders a save button when "Save as new" is selected', () => {
-    render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
+    renderModal();
 
     // "Save as new" is selected when the modal opens by default
     expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
   });
 
   test('renders an overwrite button when "Overwrite existing" is selected', () 
=> {
-    render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
+    renderModal();
 
     // Click the overwrite radio button to reveal the overwrite confirmation 
and back buttons
     const overwriteRadioBtn = screen.getByRole('radio', {
@@ -130,8 +142,7 @@ describe('SaveDatasetModal', () => {
   });
 
   test('renders the overwrite button as disabled until an existing dataset is 
selected', async () => {
-    useSelectorMock.mockReturnValue({ ...user });
-    render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
+    renderModal();
 
     // Click the overwrite radio button
     const overwriteRadioBtn = screen.getByRole('radio', {
@@ -168,8 +179,7 @@ describe('SaveDatasetModal', () => {
   });
 
   test('renders a confirm overwrite screen when overwrite is clicked', async 
() => {
-    useSelectorMock.mockReturnValue({ ...user });
-    render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
+    renderModal();
 
     // Click the overwrite radio button
     const overwriteRadioBtn = screen.getByRole('radio', {
@@ -215,11 +225,7 @@ describe('SaveDatasetModal', () => {
   });
 
   test('sends the schema when creating the dataset', async () => {
-    const dummyDispatch = jest.fn().mockResolvedValue({});
-    useDispatchMock.mockReturnValue(dummyDispatch);
-    useSelectorMock.mockReturnValue({ ...user });
-
-    render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
+    renderModal();
 
     const inputFieldText = screen.getByDisplayValue(/unimportant/i);
     fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
@@ -240,17 +246,9 @@ describe('SaveDatasetModal', () => {
   });
 
   test('sends the catalog when creating the dataset', async () => {
-    const dummyDispatch = jest.fn().mockResolvedValue({});
-    useDispatchMock.mockReturnValue(dummyDispatch);
-    useSelectorMock.mockReturnValue({ ...user });
-
-    render(
-      <SaveDatasetModal
-        {...mockedProps}
-        datasource={{ ...mockedProps.datasource, catalog: 'public' }}
-      />,
-      { useRedux: true },
-    );
+    renderModal({
+      datasource: { ...mockedProps.datasource, catalog: 'public' },
+    });
 
     const inputFieldText = screen.getByDisplayValue(/unimportant/i);
     fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
@@ -271,7 +269,7 @@ describe('SaveDatasetModal', () => {
   });
 
   test('does not renders a checkbox button when template processing is 
disabled', () => {
-    render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
+    renderModal();
     expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
   });
 
@@ -280,7 +278,7 @@ describe('SaveDatasetModal', () => {
     global.featureFlags = {
       [FeatureFlag.EnableTemplateProcessing]: true,
     };
-    render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
+    renderModal();
     expect(screen.getByRole('checkbox')).toBeInTheDocument();
   });
 
@@ -289,15 +287,11 @@ describe('SaveDatasetModal', () => {
     global.featureFlags = {
       [FeatureFlag.EnableTemplateProcessing]: true,
     };
-    const propsWithTemplateParam = {
-      ...mockedProps,
+    renderModal({
       datasource: {
         ...testQuery,
         templateParams: JSON.stringify({ my_param: 12 }),
       },
-    };
-    render(<SaveDatasetModal {...propsWithTemplateParam} />, {
-      useRedux: true,
     });
     const inputFieldText = screen.getByDisplayValue(/unimportant/i);
     fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
@@ -324,15 +318,11 @@ describe('SaveDatasetModal', () => {
     global.featureFlags = {
       [FeatureFlag.EnableTemplateProcessing]: true,
     };
-    const propsWithTemplateParam = {
-      ...mockedProps,
+    renderModal({
       datasource: {
         ...testQuery,
         templateParams: JSON.stringify({ my_param: 12 }),
       },
-    };
-    render(<SaveDatasetModal {...propsWithTemplateParam} />, {
-      useRedux: true,
     });
     const inputFieldText = screen.getByDisplayValue(/unimportant/i);
     fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
@@ -393,19 +383,11 @@ describe('SaveDatasetModal', () => {
       .spyOn(SupersetClient, 'put')
       .mockResolvedValue({ json: { result: { id: 0 } } } as any);
 
-    const dummyDispatch = jest.fn().mockResolvedValue({});
-    useDispatchMock.mockReturnValue(dummyDispatch);
-    useSelectorMock.mockReturnValue({ ...user });
-
-    const propsWithTemplateParam = {
-      ...mockedProps,
+    renderModal({
       datasource: {
         ...testQuery,
         templateParams: JSON.stringify({ my_param: 12, _filters: 'foo' }),
       },
-    };
-    render(<SaveDatasetModal {...propsWithTemplateParam} />, {
-      useRedux: true,
     });
 
     // Check the "Include Template Parameters" checkbox
@@ -443,19 +425,11 @@ describe('SaveDatasetModal', () => {
       .spyOn(SupersetClient, 'put')
       .mockResolvedValue({ json: { result: { id: 0 } } } as any);
 
-    const dummyDispatch = jest.fn().mockResolvedValue({});
-    useDispatchMock.mockReturnValue(dummyDispatch);
-    useSelectorMock.mockReturnValue({ ...user });
-
-    const propsWithTemplateParam = {
-      ...mockedProps,
+    renderModal({
       datasource: {
         ...testQuery,
         templateParams: JSON.stringify({ my_param: 12 }),
       },
-    };
-    render(<SaveDatasetModal {...propsWithTemplateParam} />, {
-      useRedux: true,
     });
 
     // Do NOT check the "Include Template Parameters" checkbox
@@ -489,12 +463,9 @@ describe('SaveDatasetModal', () => {
       'postFormData',
     );
 
-    const dummyDispatch = jest.fn().mockResolvedValue({ id: 123 });
-    useDispatchMock.mockReturnValue(dummyDispatch);
-    useSelectorMock.mockReturnValue({ ...user });
     postFormData.mockResolvedValue('chart_key_123');
 
-    render(<SaveDatasetModal {...mockedProps} />, { useRedux: true });
+    renderModal();
 
     const inputFieldText = screen.getByDisplayValue(/unimportant/i);
     fireEvent.change(inputFieldText, { target: { value: 'my dataset' } });
diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx 
b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
index c3b4ddf4991..281a2edcae2 100644
--- a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
+++ b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx
@@ -34,7 +34,6 @@ import { t } from '@apache-superset/core/translation';
 import {
   SupersetClient,
   JsonResponse,
-  JsonObject,
   QueryResponse,
   QueryFormData,
   VizType,
@@ -44,16 +43,14 @@ import {
 } from '@superset-ui/core';
 import { styled } from '@apache-superset/core/theme';
 import { extendedDayjs as dayjs } from '@superset-ui/core/utils/dates';
-import { useSelector, useDispatch } from 'react-redux';
+import { useAppDispatch, useAppSelector } from 'src/views/store';
 import rison from 'rison';
 import { createDatasource } from 'src/SqlLab/actions/sqlLab';
 import { addDangerToast } from 'src/components/MessageToasts/actions';
-import { UserWithPermissionsAndRoles as User } from 'src/types/bootstrapTypes';
 import {
   DatasetRadioState,
   EXPLORE_CHART_DEFAULT,
   DatasetOwner,
-  SqlLabRootState,
 } from 'src/SqlLab/types';
 import { mountExploreUrl } from 'src/explore/exploreUtils';
 import { postFormData } from 'src/explore/exploreUtils/formData';
@@ -221,7 +218,7 @@ export const SaveDatasetModal = ({
   openWindow = true,
   formData = {},
 }: SaveDatasetModalProps) => {
-  const defaultVizType = useSelector<SqlLabRootState, string>(
+  const defaultVizType = useAppSelector(
     state => state.common?.conf?.DEFAULT_VIZ_TYPE || VizType.Table,
   );
 
@@ -240,8 +237,8 @@ export const SaveDatasetModal = ({
   >(undefined);
   const [loading, setLoading] = useState<boolean>(false);
 
-  const user = useSelector<SqlLabRootState, User>(state => state.user);
-  const dispatch = useDispatch<(dispatch: any) => Promise<JsonObject>>();
+  const user = useAppSelector(state => state.user);
+  const dispatch = useAppDispatch();
   const [includeTemplateParameters, setIncludeTemplateParameters] =
     useState(false);
 

Reply via email to