diff --git a/superset-frontend/src/dashboard/components/gridComponents/Markdown.jsx b/superset-frontend/src/dashboard/components/gridComponents/Markdown.jsx index 59137ba3071..31415dc2706 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Markdown.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Markdown.jsx @@ -23,6 +23,7 @@ import cx from 'classnames'; import AceEditor from 'react-ace'; import 'brace/mode/markdown'; import 'brace/theme/textmate'; +import { t } from '@superset-ui/translation'; import DeleteComponentButton from '../DeleteComponentButton'; import DragDroppable from '../dnd/DragDroppable'; @@ -49,6 +50,9 @@ const propTypes = { // from redux logEvent: PropTypes.func.isRequired, + addDangerToast: PropTypes.func.isRequired, + undoLength: PropTypes.number.isRequired, + redoLength: PropTypes.number.isRequired, // grid related availableColumnCount: PropTypes.number.isRequired, @@ -65,7 +69,7 @@ const propTypes = { const defaultProps = {}; -const markdownPlaceHolder = `# ✨Markdown +const MARKDOWN_PLACE_HOLDER = `# ✨Markdown ## ✨Markdown ### ✨Markdown @@ -73,6 +77,8 @@ const markdownPlaceHolder = `# ✨Markdown Click here to edit [markdown](https://bit.ly/1dQOfRK)`; +const MARKDOWN_ERROR_MESSAGE = t('This markdown component has an error.'); + function isSafeMarkup(node) { if (node.type === 'html') { return /href="(javascript|vbscript|file):.*"/gim.test(node.value) === false; @@ -88,6 +94,8 @@ class Markdown extends React.PureComponent { markdownSource: props.component.meta.code, editor: null, editorMode: 'preview', + undoLength: props.undoLength, + redoLength: props.redoLength, }; this.renderStartTime = Logger.getTimestamp(); @@ -107,11 +115,46 @@ class Markdown extends React.PureComponent { }); } - UNSAFE_componentWillReceiveProps(nextProps) { - const nextSource = nextProps.component.meta.code; - if (this.state.markdownSource !== nextSource) { - this.setState({ markdownSource: nextSource }); + static getDerivedStateFromProps(nextProps, state) { + const { + hasError, + editorMode, + markdownSource, + undoLength, + redoLength, + } = state; + const { + component: nextComponent, + undoLength: nextUndoLength, + redoLength: nextRedoLength, + } = nextProps; + // user click undo or redo ? + if (nextUndoLength !== undoLength || nextRedoLength !== redoLength) { + return { + ...state, + undoLength: nextUndoLength, + redoLength: nextRedoLength, + markdownSource: nextComponent.meta.code, + hasError: false, + }; + } else if ( + !hasError && + editorMode === 'preview' && + nextComponent.meta.code !== markdownSource + ) { + return { + ...state, + markdownSource: nextComponent.meta.code, + }; } + + return state; + } + + static getDerivedStateFromError() { + return { + hasError: true, + }; } componentDidUpdate(prevProps) { @@ -124,6 +167,16 @@ class Markdown extends React.PureComponent { } } + componentDidCatch(error, info) { + if (this.state.editor && this.state.editorMode === 'preview') { + this.props.addDangerToast( + t( + 'This markdown component has an error. Please revert your recent changes.', + ), + ); + } + } + setEditor(editor) { editor.getSession().setUseWrapMode(true); this.setState({ @@ -139,7 +192,12 @@ class Markdown extends React.PureComponent { } handleChangeEditorMode(mode) { - if (this.state.editorMode === 'edit') { + const nextState = { + ...this.state, + editorMode: mode, + }; + + if (mode === 'preview') { const { updateComponents, component } = this.props; if (component.meta.code !== this.state.markdownSource) { updateComponents({ @@ -152,11 +210,10 @@ class Markdown extends React.PureComponent { }, }); } + nextState.hasError = false; } - this.setState(() => ({ - editorMode: mode, - })); + this.setState(nextState); } handleMarkdownChange(nextValue) { @@ -184,7 +241,7 @@ class Markdown extends React.PureComponent { // thisl allows "select all => delete" to give an empty editor typeof this.state.markdownSource === 'string' ? this.state.markdownSource - : markdownPlaceHolder + : MARKDOWN_PLACE_HOLDER } readOnly={false} onLoad={this.setEditor} @@ -193,9 +250,14 @@ class Markdown extends React.PureComponent { } renderPreviewMode() { + const { hasError } = this.state; return ( diff --git a/superset-frontend/src/dashboard/containers/DashboardComponent.jsx b/superset-frontend/src/dashboard/containers/DashboardComponent.jsx index dd2c188c8a1..e0eb38a549b 100644 --- a/superset-frontend/src/dashboard/containers/DashboardComponent.jsx +++ b/superset-frontend/src/dashboard/containers/DashboardComponent.jsx @@ -35,6 +35,7 @@ import { } from '../actions/dashboardLayout'; import { setDirectPathToChild } from '../actions/dashboardState'; import { logEvent } from '../../logger/actions'; +import { addDangerToast } from '../../messageToasts/actions'; const propTypes = { component: componentShape.isRequired, @@ -66,6 +67,8 @@ function mapStateToProps( component, parentComponent: dashboardLayout[parentId], editMode: dashboardState.editMode, + undoLength: undoableLayout.past.length, + redoLength: undoableLayout.future.length, filters: getActiveFilters(), directPathToChild: dashboardState.directPathToChild, directPathLastUpdated: dashboardState.directPathLastUpdated, @@ -97,6 +100,7 @@ function mapStateToProps( function mapDispatchToProps(dispatch) { return bindActionCreators( { + addDangerToast, createComponent, deleteComponent, updateComponents,