mirror of https://github.com/apache/superset.git
[dashboard] Add alert on user delete root level tab (#5771)
This commit is contained in:
parent
5616d7bdd7
commit
0c98ecb6d1
|
@ -6,7 +6,7 @@ import { expect } from 'chai';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
|
||||||
import DashboardComponent from '../../../../../src/dashboard/containers/DashboardComponent';
|
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 DragDroppable from '../../../../../src/dashboard/components/dnd/DragDroppable';
|
||||||
import EditableTitle from '../../../../../src/components/EditableTitle';
|
import EditableTitle from '../../../../../src/components/EditableTitle';
|
||||||
import WithPopoverMenu from '../../../../../src/dashboard/components/menu/WithPopoverMenu';
|
import WithPopoverMenu from '../../../../../src/dashboard/components/menu/WithPopoverMenu';
|
||||||
|
@ -86,14 +86,14 @@ describe('Tabs', () => {
|
||||||
expect(wrapper.find(WithPopoverMenu)).to.have.length(1);
|
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();
|
let wrapper = setup();
|
||||||
wrapper.find(WithPopoverMenu).simulate('click'); // focus
|
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 = setup({ editMode: true });
|
||||||
wrapper.find(WithPopoverMenu).simulate('click');
|
wrapper.find(WithPopoverMenu).simulate('click');
|
||||||
expect(wrapper.find(DeleteComponentButton)).to.have.length(1);
|
expect(wrapper.find(DeleteComponentModal)).to.have.length(1);
|
||||||
|
|
||||||
wrapper = setup({
|
wrapper = setup({
|
||||||
editMode: true,
|
editMode: true,
|
||||||
|
@ -103,16 +103,18 @@ describe('Tabs', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
wrapper.find(WithPopoverMenu).simulate('click');
|
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 deleteComponent = sinon.spy();
|
||||||
const wrapper = setup({ editMode: true, deleteComponent });
|
const wrapper = setup({ editMode: true, deleteComponent });
|
||||||
wrapper.find(WithPopoverMenu).simulate('click'); // focus
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import Button from './Button';
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
animation: PropTypes.bool,
|
animation: PropTypes.bool,
|
||||||
triggerNode: PropTypes.node.isRequired,
|
triggerNode: PropTypes.node.isRequired,
|
||||||
modalTitle: PropTypes.node.isRequired,
|
modalTitle: PropTypes.node,
|
||||||
modalBody: PropTypes.node, // not required because it can be generated by beforeOpen
|
modalBody: PropTypes.node, // not required because it can be generated by beforeOpen
|
||||||
modalFooter: PropTypes.node,
|
modalFooter: PropTypes.node,
|
||||||
beforeOpen: PropTypes.func,
|
beforeOpen: PropTypes.func,
|
||||||
|
@ -28,6 +28,7 @@ const defaultProps = {
|
||||||
isMenuItem: false,
|
isMenuItem: false,
|
||||||
bsSize: null,
|
bsSize: null,
|
||||||
className: '',
|
className: '',
|
||||||
|
modalTitle: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class ModalTrigger extends React.Component {
|
export default class ModalTrigger extends React.Component {
|
||||||
|
@ -59,9 +60,11 @@ export default class ModalTrigger extends React.Component {
|
||||||
bsSize={this.props.bsSize}
|
bsSize={this.props.bsSize}
|
||||||
className={this.props.className}
|
className={this.props.className}
|
||||||
>
|
>
|
||||||
|
{this.props.modalTitle &&
|
||||||
<Modal.Header closeButton>
|
<Modal.Header closeButton>
|
||||||
<Modal.Title>{this.props.modalTitle}</Modal.Title>
|
<Modal.Title>{this.props.modalTitle}</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
|
}
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
{this.props.modalBody}
|
{this.props.modalBody}
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
|
|
|
@ -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 (
|
||||||
|
<ModalTrigger
|
||||||
|
ref={this.setModalRef}
|
||||||
|
triggerNode={this.props.triggerNode}
|
||||||
|
modalBody={
|
||||||
|
<div className="delete-component-modal">
|
||||||
|
<h1>{t('Delete dashboard tab?')}</h1>
|
||||||
|
<div>
|
||||||
|
Deleting a tab will remove all content within it. You may still
|
||||||
|
reverse this action with the <b>undo</b> button (cmd + z) until
|
||||||
|
you save your changes.
|
||||||
|
</div>
|
||||||
|
<div className="delete-modal-actions-container">
|
||||||
|
<Button onClick={this.close}>{t('Cancel')}</Button>
|
||||||
|
<Button bsStyle="primary" onClick={this.deleteTab}>
|
||||||
|
{t('Delete')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteComponentModal.propTypes = propTypes;
|
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||||
import DashboardComponent from '../../containers/DashboardComponent';
|
import DashboardComponent from '../../containers/DashboardComponent';
|
||||||
import DragDroppable from '../dnd/DragDroppable';
|
import DragDroppable from '../dnd/DragDroppable';
|
||||||
import EditableTitle from '../../../components/EditableTitle';
|
import EditableTitle from '../../../components/EditableTitle';
|
||||||
import DeleteComponentButton from '../DeleteComponentButton';
|
import DeleteComponentModal from '../DeleteComponentModal';
|
||||||
import WithPopoverMenu from '../menu/WithPopoverMenu';
|
import WithPopoverMenu from '../menu/WithPopoverMenu';
|
||||||
import { componentShape } from '../../util/propShapes';
|
import { componentShape } from '../../util/propShapes';
|
||||||
import { DASHBOARD_ROOT_DEPTH } from '../../util/constants';
|
import { DASHBOARD_ROOT_DEPTH } from '../../util/constants';
|
||||||
|
@ -178,6 +178,11 @@ export default class Tab extends React.PureComponent {
|
||||||
renderTab() {
|
renderTab() {
|
||||||
const { isFocused } = this.state;
|
const { isFocused } = this.state;
|
||||||
const { component, parentComponent, index, depth, editMode } = this.props;
|
const { component, parentComponent, index, depth, editMode } = this.props;
|
||||||
|
const deleteTabIcon = (
|
||||||
|
<div className="icon-button">
|
||||||
|
<span className="fa fa-trash" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragDroppable
|
<DragDroppable
|
||||||
|
@ -201,7 +206,8 @@ export default class Tab extends React.PureComponent {
|
||||||
parentComponent.children.length <= 1
|
parentComponent.children.length <= 1
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
<DeleteComponentButton
|
<DeleteComponentModal
|
||||||
|
triggerNode={deleteTabIcon}
|
||||||
onDelete={this.handleDeleteComponent}
|
onDelete={this.handleDeleteComponent}
|
||||||
/>,
|
/>,
|
||||||
]
|
]
|
||||||
|
|
|
@ -20,7 +20,8 @@ const defaultProps = {
|
||||||
onPressDelete() {},
|
onPressDelete() {},
|
||||||
menuItems: [],
|
menuItems: [],
|
||||||
isFocused: false,
|
isFocused: false,
|
||||||
shouldFocus: (event, container) => container.contains(event.target),
|
shouldFocus: (event, container) =>
|
||||||
|
container && container.contains(event.target),
|
||||||
style: null,
|
style: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -36,19 +37,19 @@ class WithPopoverMenu extends React.PureComponent {
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (nextProps.editMode && nextProps.isFocused && !this.state.isFocused) {
|
if (nextProps.editMode && nextProps.isFocused && !this.state.isFocused) {
|
||||||
document.addEventListener('click', this.handleClick, true);
|
document.addEventListener('click', this.handleClick);
|
||||||
document.addEventListener('drag', this.handleClick, true);
|
document.addEventListener('drag', this.handleClick);
|
||||||
this.setState({ isFocused: true });
|
this.setState({ isFocused: true });
|
||||||
} else if (this.state.isFocused && !nextProps.editMode) {
|
} else if (this.state.isFocused && !nextProps.editMode) {
|
||||||
document.removeEventListener('click', this.handleClick, true);
|
document.removeEventListener('click', this.handleClick);
|
||||||
document.removeEventListener('drag', this.handleClick, true);
|
document.removeEventListener('drag', this.handleClick);
|
||||||
this.setState({ isFocused: false });
|
this.setState({ isFocused: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener('click', this.handleClick, true);
|
document.removeEventListener('click', this.handleClick);
|
||||||
document.removeEventListener('drag', this.handleClick, true);
|
document.removeEventListener('drag', this.handleClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
setRef(ref) {
|
setRef(ref) {
|
||||||
|
@ -69,15 +70,15 @@ class WithPopoverMenu extends React.PureComponent {
|
||||||
if (!disableClick && shouldFocus && !this.state.isFocused) {
|
if (!disableClick && shouldFocus && !this.state.isFocused) {
|
||||||
// if not focused, set focus and add a window event listener to capture outside clicks
|
// if not focused, set focus and add a window event listener to capture outside clicks
|
||||||
// this enables us to not set a click listener for ever item on a dashboard
|
// this enables us to not set a click listener for ever item on a dashboard
|
||||||
document.addEventListener('click', this.handleClick, true);
|
document.addEventListener('click', this.handleClick);
|
||||||
document.addEventListener('drag', this.handleClick, true);
|
document.addEventListener('drag', this.handleClick);
|
||||||
this.setState(() => ({ isFocused: true }));
|
this.setState(() => ({ isFocused: true }));
|
||||||
if (onChangeFocus) {
|
if (onChangeFocus) {
|
||||||
onChangeFocus(true);
|
onChangeFocus(true);
|
||||||
}
|
}
|
||||||
} else if (!shouldFocus && this.state.isFocused) {
|
} else if (!shouldFocus && this.state.isFocused) {
|
||||||
document.removeEventListener('click', this.handleClick, true);
|
document.removeEventListener('click', this.handleClick);
|
||||||
document.removeEventListener('drag', this.handleClick, true);
|
document.removeEventListener('drag', this.handleClick);
|
||||||
this.setState(() => ({ isFocused: false }));
|
this.setState(() => ({ isFocused: false }));
|
||||||
if (onChangeFocus) {
|
if (onChangeFocus) {
|
||||||
onChangeFocus(false);
|
onChangeFocus(false);
|
||||||
|
|
|
@ -129,12 +129,40 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal img.loading {
|
.modal {
|
||||||
|
img.loading {
|
||||||
width: 50px;
|
width: 50px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 24px 24px 29px 24px;
|
||||||
|
|
||||||
|
div {
|
||||||
|
margin-top: 24px;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-modal-actions-container {
|
||||||
|
.btn {
|
||||||
|
margin-right: 16px;
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-primary {
|
||||||
|
background: @pink !important;
|
||||||
|
border-color: @pink !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.react-bs-container-body {
|
.react-bs-container-body {
|
||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
Loading…
Reference in New Issue