diff --git a/superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.test.jsx b/superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.test.jsx index 2282ec0f5251..5f449cc3d29b 100644 --- a/superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.test.jsx +++ b/superset-frontend/src/components/Datasource/DatasourceModal/DatasourceModal.test.jsx @@ -122,7 +122,7 @@ describe('DatasourceModal', () => { }); test('should render error dialog', async () => { - jest + const putSpy = jest .spyOn(SupersetClient, 'put') .mockRejectedValue(new Error('Something went wrong')); await act(async () => { @@ -135,5 +135,172 @@ describe('DatasourceModal', () => { const errorTitle = await screen.findByText('Error saving dataset'); expect(errorTitle).toBeInTheDocument(); }); + putSpy.mockRestore(); + }); + + test('shows sync columns checkbox when SQL changes', async () => { + cleanup(); + const datasourceWithSQL = { + ...mockedProps.datasource, + sql: 'SELECT * FROM original_table', + }; + const modifiedDatasource = { + ...datasourceWithSQL, + sql: 'SELECT * FROM new_table', // Different SQL to trigger checkbox + }; + + const { rerender } = render( + , + { store, useRouter: true }, + ); + + // Update with modified SQL + rerender( + , + ); + + const saveButton = screen.getByTestId('datasource-modal-save'); + fireEvent.click(saveButton); + + // Wait for confirmation modal to appear + await waitFor(() => { + expect(screen.getByText('Confirm save')).toBeInTheDocument(); + }); + + // Checkbox should be present and checked by default when SQL changes + const checkbox = await screen.findByRole('checkbox'); + expect(checkbox).toBeInTheDocument(); + expect(checkbox).toBeChecked(); + + // Should show the sync columns message + expect(screen.getByText('Automatically sync columns')).toBeInTheDocument(); + }); + + test('syncs columns when checkbox is checked and submits with override_columns=true', async () => { + const datasourceWithSQL = { + ...mockedProps.datasource, + sql: 'SELECT * FROM original_table', + }; + const modifiedDatasource = { + ...datasourceWithSQL, + sql: 'SELECT * FROM new_table', + }; + + // Render with the initial datasource + cleanup(); + fetchMock.reset(); + fetchMock.post(SAVE_ENDPOINT, SAVE_PAYLOAD); + fetchMock.put(SAVE_DATASOURCE_ENDPOINT, {}); + fetchMock.get(GET_DATASOURCE_ENDPOINT, { result: {} }); + fetchMock.get(GET_DATABASE_ENDPOINT, { result: [] }); + + const { rerender } = render( + , + { store, useRouter: true }, + ); + + // Update with modified SQL to trigger checkbox + rerender( + , + ); + + const saveButton = screen.getByTestId('datasource-modal-save'); + fireEvent.click(saveButton); + + // Wait for confirmation modal to appear + await waitFor(() => { + expect(screen.getByText('Confirm save')).toBeInTheDocument(); + }); + + // Checkbox should be present and checked by default when SQL changes + const checkbox = await screen.findByRole('checkbox'); + expect(checkbox).toBeChecked(); + + // Click OK to submit + const okButton = screen.getByRole('button', { name: 'OK' }); + fireEvent.click(okButton); + + // Verify the PUT request was made with override_columns=true + await waitFor(() => { + const putCalls = fetchMock + .calls() + .filter( + call => + call[0].includes('/api/v1/dataset/7') && + call[0].includes('override_columns') && + call[1]?.method === 'PUT', + ); + expect(putCalls.length).toBeGreaterThan(0); + expect(putCalls[putCalls.length - 1][0]).toContain( + 'override_columns=true', + ); + }); + }); + + test('does not sync columns when checkbox is unchecked and submits with override_columns=false', async () => { + const datasourceWithSQL = { + ...mockedProps.datasource, + sql: 'SELECT * FROM original_table', + }; + const modifiedDatasource = { + ...datasourceWithSQL, + sql: 'SELECT * FROM new_table', + }; + + // Render with the initial datasource + cleanup(); + fetchMock.reset(); + fetchMock.post(SAVE_ENDPOINT, SAVE_PAYLOAD); + fetchMock.put(SAVE_DATASOURCE_ENDPOINT, {}); + fetchMock.get(GET_DATASOURCE_ENDPOINT, { result: {} }); + fetchMock.get(GET_DATABASE_ENDPOINT, { result: [] }); + + const { rerender } = render( + , + { store, useRouter: true }, + ); + + // Update with modified SQL to trigger checkbox + rerender( + , + ); + + const saveButton = screen.getByTestId('datasource-modal-save'); + fireEvent.click(saveButton); + + // Wait for confirmation modal to appear + await waitFor(() => { + expect(screen.getByText('Confirm save')).toBeInTheDocument(); + }); + + // Checkbox should be present and checked by default when SQL changes + const checkbox = await screen.findByRole('checkbox'); + expect(checkbox).toBeChecked(); + + // Uncheck the checkbox + fireEvent.click(checkbox); + + // Verify checkbox is now unchecked + expect(checkbox).not.toBeChecked(); + + // Click OK to submit + const okButton = screen.getByRole('button', { name: 'OK' }); + fireEvent.click(okButton); + + // Verify the PUT request was made with override_columns=false + await waitFor(() => { + const putCalls = fetchMock + .calls() + .filter( + call => + call[0].includes('/api/v1/dataset/7') && + call[0].includes('override_columns') && + call[1]?.method === 'PUT', + ); + expect(putCalls.length).toBeGreaterThan(0); + expect(putCalls[putCalls.length - 1][0]).toContain( + 'override_columns=false', + ); + }); }); }); diff --git a/superset-frontend/src/components/Datasource/DatasourceModal/index.tsx b/superset-frontend/src/components/Datasource/DatasourceModal/index.tsx index 36bd4f815fda..598aca2484d0 100644 --- a/superset-frontend/src/components/Datasource/DatasourceModal/index.tsx +++ b/superset-frontend/src/components/Datasource/DatasourceModal/index.tsx @@ -108,6 +108,7 @@ const DatasourceModal: FunctionComponent = ({ const [isSaving, setIsSaving] = useState(false); const [isEditing, setIsEditing] = useState(false); const [modal, contextHolder] = Modal.useModal(); + const [confirmModalOpen, setConfirmModalOpen] = useState(false); const buildPayload = (datasource: Record) => { const payload: Record = { table_name: datasource.table_name, @@ -275,7 +276,7 @@ const DatasourceModal: FunctionComponent = ({ { - setSyncColumns(!syncColumns); + setSyncColumns(prev => !prev); }} /> = ({ }, [datasource.sql, currentDatasource.sql]); const onClickSave = () => { - modal.confirm({ - title: t('Confirm save'), - content: getSaveDialog(), - onOk: onConfirmSave, - icon: null, - okText: t('OK'), - cancelText: t('Cancel'), - }); + setConfirmModalOpen(true); + }; + + const handleConfirmModalClose = () => { + setConfirmModalOpen(false); + }; + + const handleConfirmSave = async () => { + await onConfirmSave(); + // Note: on success, onConfirmSave calls onHide() which closes parent modal + // On error, confirmModal stays open so user can see the error and try again or cancel }; return ( @@ -371,6 +375,16 @@ const DatasourceModal: FunctionComponent = ({ currencies={currencies} /> {contextHolder} + + {getSaveDialog()} + ); };