refactor: Use Modals from Antd instead of react-bootstrap (#11330)

* Refactor ModalTrigger to use antd modal

* Refactor a few components

* dynamic width

* Fix unit tests

* Use i18n for button text
This commit is contained in:
Kamil Gabryjelski 2020-10-26 19:20:10 +01:00 committed by GitHub
parent 144b279aa2
commit 894ca3c09b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 94 additions and 53 deletions

View File

@ -18,7 +18,7 @@
*/
import React from 'react';
import { mount } from 'enzyme';
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
import CssEditor from 'src/dashboard/components/CssEditor';
describe('CssEditor', () => {
@ -29,7 +29,12 @@ describe('CssEditor', () => {
expect(React.isValidElement(<CssEditor {...mockedProps} />)).toBe(true);
});
it('renders the trigger node', () => {
const wrapper = mount(<CssEditor {...mockedProps} />);
const wrapper = mount(<CssEditor {...mockedProps} />, {
wrappingComponent: ThemeProvider,
wrappingComponentProps: {
theme: supersetTheme,
},
});
expect(wrapper.find('.fa-edit')).toExist();
});
});

View File

@ -22,6 +22,15 @@ import { mount, shallow } from 'enzyme';
import ModalTrigger from 'src/components/ModalTrigger';
import RefreshIntervalModal from 'src/dashboard/components/RefreshIntervalModal';
import { Alert } from 'react-bootstrap';
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
const getMountWrapper = props =>
mount(<RefreshIntervalModal {...props} />, {
wrappingComponent: ThemeProvider,
wrappingComponentProps: {
theme: supersetTheme,
},
});
describe('RefreshIntervalModal', () => {
const mockedProps = {
@ -36,15 +45,15 @@ describe('RefreshIntervalModal', () => {
).toBe(true);
});
it('renders the trigger node', () => {
const wrapper = mount(<RefreshIntervalModal {...mockedProps} />);
const wrapper = getMountWrapper(mockedProps);
expect(wrapper.find('.fa-edit')).toExist();
});
it('should render a interval seconds', () => {
const wrapper = mount(<RefreshIntervalModal {...mockedProps} />);
const wrapper = getMountWrapper(mockedProps);
expect(wrapper.prop('refreshFrequency')).toEqual(10);
});
it('should change refreshFrequency with edit mode', () => {
const wrapper = mount(<RefreshIntervalModal {...mockedProps} />);
const wrapper = getMountWrapper(mockedProps);
wrapper.instance().handleFrequencyChange({ value: 30 });
wrapper.instance().onSave();
expect(mockedProps.onChange).toHaveBeenCalled();

View File

@ -126,7 +126,7 @@ describe('Tabs', () => {
wrapper.find(WithPopoverMenu).simulate('click'); // focus
wrapper.find('.icon-button').simulate('click');
const modal = document.getElementsByClassName('modal');
const modal = document.getElementsByClassName('ant-modal');
expect(modal).toHaveLength(1);
expect(deleteComponent.callCount).toBe(0);
});

View File

@ -21,6 +21,7 @@ import { mount } from 'enzyme';
import ModalTrigger from 'src/components/ModalTrigger';
import { DisplayQueryButton } from 'src/explore/components/DisplayQueryButton';
import { MenuItem } from 'react-bootstrap';
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
describe('DisplayQueryButton', () => {
const defaultProps = {
@ -43,7 +44,12 @@ describe('DisplayQueryButton', () => {
);
});
it('renders a dropdown', () => {
const wrapper = mount(<DisplayQueryButton {...defaultProps} />);
const wrapper = mount(<DisplayQueryButton {...defaultProps} />, {
wrappingComponent: ThemeProvider,
wrappingComponentProps: {
theme: supersetTheme,
},
});
expect(wrapper.find(ModalTrigger)).toHaveLength(3);
expect(wrapper.find(MenuItem)).toHaveLength(5);
});

View File

@ -22,6 +22,7 @@ import { mount, shallow } from 'enzyme';
import HighlightedSql from 'src/SqlLab/components/HighlightedSql';
import ModalTrigger from 'src/components/ModalTrigger';
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
describe('HighlightedSql', () => {
const sql =
@ -45,6 +46,12 @@ describe('HighlightedSql', () => {
shrink
maxWidth={5}
/>,
{
wrappingComponent: ThemeProvider,
wrappingComponentProps: {
theme: supersetTheme,
},
},
);
const pre = wrapper.find('pre');
expect(pre).toHaveLength(1);

View File

@ -20,6 +20,7 @@ import React from 'react';
import { mount, shallow } from 'enzyme';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import { supersetTheme, ThemeProvider } from '@superset-ui/core';
import Link from 'src/components/Link';
import TableElement from 'src/SqlLab/components/TableElement';
@ -54,6 +55,12 @@ describe('TableElement', () => {
<Provider store={store}>
<TableElement {...mockedProps} />
</Provider>,
{
wrappingComponent: ThemeProvider,
wrappingComponentProps: {
theme: supersetTheme,
},
},
);
});
it('sorts columns', () => {
@ -71,6 +78,12 @@ describe('TableElement', () => {
<Provider store={store}>
<TableElement {...mockedProps} />
</Provider>,
{
wrappingComponent: ThemeProvider,
wrappingComponentProps: {
theme: supersetTheme,
},
},
);
expect(mockedActions.collapseTable.called).toBe(false);
wrapper.find('.table-name').simulate('click');

View File

@ -142,7 +142,6 @@ const QueryTable = props => {
if (q.resultsKey) {
q.output = (
<ModalTrigger
bsSize="large"
className="ResultsModal"
triggerNode={
<Label bsStyle="info" className="pointer">
@ -161,6 +160,7 @@ const QueryTable = props => {
displayLimit={props.displayLimit}
/>
}
responsive
/>
);
} else {

View File

@ -466,11 +466,11 @@ div.tablePopover {
display: inline-block;
}
.ResultsModal .modal-body {
min-height: 600px;
.ResultsModal .ant-modal-body {
min-height: 560px;
}
.modal-body {
.ant-modal-body {
overflow: auto;
}

View File

@ -18,9 +18,10 @@
*/
import React from 'react';
import { isNil } from 'lodash';
import { styled, t } from '@superset-ui/core';
import { styled, SupersetThemeProps, t } from '@superset-ui/core';
import { Modal as BaseModal } from 'src/common/components';
import Button from 'src/components/Button';
import { css } from '@emotion/core';
interface ModalProps {
className?: string;
@ -33,12 +34,25 @@ interface ModalProps {
show: boolean;
title: React.ReactNode;
width?: string;
maxWidth?: string;
responsive?: boolean;
hideFooter?: boolean;
centered?: boolean;
footer?: React.ReactNode;
}
const StyledModal = styled(BaseModal)`
interface StyledModalProps extends SupersetThemeProps {
maxWidth?: string;
responsive?: boolean;
}
const StyledModal = styled(BaseModal)<StyledModalProps>`
${({ responsive, maxWidth }) =>
responsive &&
css`
max-width: ${maxWidth ?? '900px'};
`}
.ant-modal-header {
background-color: ${({ theme }) => theme.colors.grayscale.light4};
border-radius: ${({ theme }) => theme.borderRadius}px
@ -94,11 +108,13 @@ export default function Modal({
disablePrimaryButton = false,
onHide,
onHandledPrimaryAction,
primaryButtonName,
primaryButtonName = t('OK'),
primaryButtonType = 'primary',
show,
title,
width,
maxWidth,
responsive = false,
centered,
footer,
hideFooter,
@ -121,12 +137,15 @@ export default function Modal({
]
: footer;
const modalWidth = width || responsive ? 'calc(100vw - 24px)' : '600px';
return (
<StyledModal
centered={!!centered}
onOk={onHandledPrimaryAction}
onCancel={onHide}
width={width || '600px'}
width={modalWidth}
maxWidth={maxWidth}
responsive={responsive}
visible={show}
title={title}
closeIcon={

View File

@ -222,11 +222,10 @@ export default class AlteredSliceTag extends React.Component {
// differences in the slice
return (
<ModalTrigger
animation
triggerNode={this.renderTriggerNode()}
modalTitle={t('Chart changes')}
bsSize="large"
modalBody={this.renderModalBody()}
responsive
/>
);
}

View File

@ -18,13 +18,12 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import { Modal, MenuItem } from 'react-bootstrap';
import { MenuItem } from 'react-bootstrap';
import Modal from 'src/common/components/Modal';
import Button from 'src/components/Button';
const propTypes = {
dialogClassName: PropTypes.string,
animation: PropTypes.bool,
triggerNode: PropTypes.node.isRequired,
modalTitle: PropTypes.node,
modalBody: PropTypes.node, // not required because it can be generated by beforeOpen
@ -33,19 +32,18 @@ const propTypes = {
onExit: PropTypes.func,
isButton: PropTypes.bool,
isMenuItem: PropTypes.bool,
bsSize: PropTypes.oneOf(['large', 'small']), // react-bootstrap also supports 'sm', 'lg' but we're keeping it simple.
className: PropTypes.string,
tooltip: PropTypes.string,
backdrop: PropTypes.oneOf(['static', true, false]),
width: PropTypes.string,
maxWidth: PropTypes.string,
responsive: PropTypes.bool,
};
const defaultProps = {
animation: true,
beforeOpen: () => {},
onExit: () => {},
isButton: false,
isMenuItem: false,
bsSize: null,
className: '',
modalTitle: '',
};
@ -73,24 +71,18 @@ export default class ModalTrigger extends React.Component {
renderModal() {
return (
<Modal
dialogClassName={this.props.dialogClassName}
animation={this.props.animation}
wrapClassName={this.props.dialogClassName}
className={this.props.className}
show={this.state.showModal}
onHide={this.close}
onExit={this.props.onExit}
bsSize={this.props.bsSize}
className={this.props.className}
backdrop={this.props.backdrop}
afterClose={this.props.onExit}
title={this.props.modalTitle}
footer={this.props.modalFooter}
hideFooter={!this.props.modalFooter}
maxWidth={this.props.maxWidth}
responsive={this.props.responsive}
>
{this.props.modalTitle && (
<Modal.Header closeButton>
<Modal.Title>{this.props.modalTitle}</Modal.Title>
</Modal.Header>
)}
<Modal.Body>{this.props.modalBody}</Modal.Body>
{this.props.modalFooter && (
<Modal.Footer>{this.props.modalFooter}</Modal.Footer>
)}
{this.props.modalBody}
</Modal>
);
}

View File

@ -58,16 +58,12 @@ SyntaxHighlighter.registerLanguage('json', jsonSyntax);
const propTypes = {
onOpenInEditor: PropTypes.func,
animation: PropTypes.bool,
queryResponse: PropTypes.object,
chartStatus: PropTypes.string,
chartHeight: PropTypes.string.isRequired,
latestQueryFormData: PropTypes.object.isRequired,
slice: PropTypes.object,
};
const defaultProps = {
animation: true,
};
export class DisplayQueryButton extends React.PureComponent {
constructor(props) {
@ -234,7 +230,7 @@ export class DisplayQueryButton extends React.PureComponent {
}
render() {
const { animation, chartHeight, slice } = this.props;
const { chartHeight, slice } = this.props;
return (
<DropdownButton
noCaret
@ -259,38 +255,34 @@ export class DisplayQueryButton extends React.PureComponent {
show={this.state.isPropertiesModalOpen}
onHide={this.closePropertiesModal}
onSave={this.props.sliceUpdated}
animation={animation}
/>
</>
)}
<ModalTrigger
isMenuItem
animation={animation}
triggerNode={
<span data-test="view-query-menu-item">{t('View query')}</span>
}
modalTitle={t('View query')}
bsSize="large"
beforeOpen={() => this.beforeOpen('query')}
modalBody={this.renderQueryModalBody()}
responsive
/>
<ModalTrigger
isMenuItem
animation={animation}
triggerNode={<span>{t('View results')}</span>}
modalTitle={t('View results')}
bsSize="large"
beforeOpen={() => this.beforeOpen('results')}
modalBody={this.renderResultsModalBody()}
responsive
/>
<ModalTrigger
isMenuItem
animation={animation}
triggerNode={<span>{t('View samples')}</span>}
modalTitle={t('View samples')}
bsSize="large"
beforeOpen={() => this.beforeOpen('samples')}
modalBody={this.renderSamplesModalBody()}
responsive
/>
{this.state.sqlSupported && (
<MenuItem eventKey="3" onClick={this.redirectSQLLab.bind(this)}>
@ -315,7 +307,6 @@ export class DisplayQueryButton extends React.PureComponent {
}
DisplayQueryButton.propTypes = propTypes;
DisplayQueryButton.defaultProps = defaultProps;
function mapDispatchToProps(dispatch) {
return bindActionCreators({ sliceUpdated }, dispatch);

View File

@ -124,7 +124,6 @@ export default class TextAreaControl extends React.Component {
{this.renderEditor()}
{this.props.offerEditInModal && (
<ModalTrigger
bsSize="large"
modalTitle={controlHeader}
triggerNode={
<Button buttonSize="small" className="m-t-5">
@ -133,6 +132,7 @@ export default class TextAreaControl extends React.Component {
</Button>
}
modalBody={this.renderModalBody(true)}
responsive
/>
)}
</div>