diff --git a/superset/assets/spec/javascripts/dashboard/components/gridComponents/Tab_spec.jsx b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Tab_spec.jsx
index a984565b4c4fb..fae59b2590e36 100644
--- a/superset/assets/spec/javascripts/dashboard/components/gridComponents/Tab_spec.jsx
+++ b/superset/assets/spec/javascripts/dashboard/components/gridComponents/Tab_spec.jsx
@@ -6,7 +6,7 @@ import { expect } from 'chai';
import sinon from 'sinon';
import DashboardComponent from '../../../../../src/dashboard/containers/DashboardComponent';
-import DeleteComponentButton from '../../../../../src/dashboard/components/DeleteComponentButton';
+import DeleteComponentModal from '../../../../../src/dashboard/components/DeleteComponentModal';
import DragDroppable from '../../../../../src/dashboard/components/dnd/DragDroppable';
import EditableTitle from '../../../../../src/components/EditableTitle';
import WithPopoverMenu from '../../../../../src/dashboard/components/menu/WithPopoverMenu';
@@ -86,14 +86,14 @@ describe('Tabs', () => {
expect(wrapper.find(WithPopoverMenu)).to.have.length(1);
});
- it('should render a DeleteComponentButton when focused if its not the only tab', () => {
+ it('should render a DeleteComponentModal when focused if its not the only tab', () => {
let wrapper = setup();
wrapper.find(WithPopoverMenu).simulate('click'); // focus
- expect(wrapper.find(DeleteComponentButton)).to.have.length(0);
+ expect(wrapper.find(DeleteComponentModal)).to.have.length(0);
wrapper = setup({ editMode: true });
wrapper.find(WithPopoverMenu).simulate('click');
- expect(wrapper.find(DeleteComponentButton)).to.have.length(1);
+ expect(wrapper.find(DeleteComponentModal)).to.have.length(1);
wrapper = setup({
editMode: true,
@@ -103,16 +103,18 @@ describe('Tabs', () => {
},
});
wrapper.find(WithPopoverMenu).simulate('click');
- expect(wrapper.find(DeleteComponentButton)).to.have.length(0);
+ expect(wrapper.find(DeleteComponentModal)).to.have.length(0);
});
- it('should call deleteComponent when deleted', () => {
+ it('should show modal when clicked delete icon', () => {
const deleteComponent = sinon.spy();
const wrapper = setup({ editMode: true, deleteComponent });
wrapper.find(WithPopoverMenu).simulate('click'); // focus
- wrapper.find(DeleteComponentButton).simulate('click');
+ wrapper.find('.icon-button').simulate('click');
- expect(deleteComponent.callCount).to.equal(1);
+ const modal = document.getElementsByClassName('modal');
+ expect(modal).to.have.length(1);
+ expect(deleteComponent.callCount).to.equal(0);
});
});
diff --git a/superset/assets/src/components/ModalTrigger.jsx b/superset/assets/src/components/ModalTrigger.jsx
index 67a83e6c21627..83e8db361f3da 100644
--- a/superset/assets/src/components/ModalTrigger.jsx
+++ b/superset/assets/src/components/ModalTrigger.jsx
@@ -8,7 +8,7 @@ import Button from './Button';
const propTypes = {
animation: PropTypes.bool,
triggerNode: PropTypes.node.isRequired,
- modalTitle: PropTypes.node.isRequired,
+ modalTitle: PropTypes.node,
modalBody: PropTypes.node, // not required because it can be generated by beforeOpen
modalFooter: PropTypes.node,
beforeOpen: PropTypes.func,
@@ -28,6 +28,7 @@ const defaultProps = {
isMenuItem: false,
bsSize: null,
className: '',
+ modalTitle: '',
};
export default class ModalTrigger extends React.Component {
@@ -59,9 +60,11 @@ export default class ModalTrigger extends React.Component {
bsSize={this.props.bsSize}
className={this.props.className}
>
-
- {this.props.modalTitle}
-
+ {this.props.modalTitle &&
+
+ {this.props.modalTitle}
+
+ }
{this.props.modalBody}
diff --git a/superset/assets/src/dashboard/components/DeleteComponentModal.jsx b/superset/assets/src/dashboard/components/DeleteComponentModal.jsx
new file mode 100644
index 0000000000000..ea7721c037e12
--- /dev/null
+++ b/superset/assets/src/dashboard/components/DeleteComponentModal.jsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Button } from 'react-bootstrap';
+
+import ModalTrigger from '../../components/ModalTrigger';
+import { t } from '../../locales';
+
+const propTypes = {
+ triggerNode: PropTypes.node.isRequired,
+ onDelete: PropTypes.func.isRequired,
+};
+
+export default class DeleteComponentModal extends React.PureComponent {
+ constructor(props) {
+ super(props);
+
+ this.modal = null;
+ this.close = this.close.bind(this);
+ this.deleteTab = this.deleteTab.bind(this);
+ this.setModalRef = this.setModalRef.bind(this);
+ }
+
+ setModalRef(ref) {
+ this.modal = ref;
+ }
+
+ close() {
+ this.modal.close();
+ }
+
+ deleteTab() {
+ this.modal.close();
+ this.props.onDelete();
+ }
+
+ render() {
+ return (
+
+
{t('Delete dashboard tab?')}
+
+ Deleting a tab will remove all content within it. You may still
+ reverse this action with the undo button (cmd + z) until
+ you save your changes.
+
+
+
+
+
+
+ }
+ />
+ );
+ }
+}
+
+DeleteComponentModal.propTypes = propTypes;
diff --git a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
index b91d8089fd2fe..dad3be4c72bb5 100644
--- a/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
+++ b/superset/assets/src/dashboard/components/gridComponents/Tab.jsx
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import DashboardComponent from '../../containers/DashboardComponent';
import DragDroppable from '../dnd/DragDroppable';
import EditableTitle from '../../../components/EditableTitle';
-import DeleteComponentButton from '../DeleteComponentButton';
+import DeleteComponentModal from '../DeleteComponentModal';
import WithPopoverMenu from '../menu/WithPopoverMenu';
import { componentShape } from '../../util/propShapes';
import { DASHBOARD_ROOT_DEPTH } from '../../util/constants';
@@ -178,6 +178,11 @@ export default class Tab extends React.PureComponent {
renderTab() {
const { isFocused } = this.state;
const { component, parentComponent, index, depth, editMode } = this.props;
+ const deleteTabIcon = (
+