diff --git a/src/App.jsx b/src/App.jsx index dca3fdc03b5..110d772f203 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -24,26 +24,9 @@ import EditorContentOriginPlugin from './plugins/editor-content-origin/index.js' import EditorContentTypePlugin from './plugins/editor-content-type/index.js'; import EditorContentPersistencePlugin from './plugins/editor-content-persistence/index.js'; import EditorContentFixturesPlugin from './plugins/editor-content-fixtures/index.js'; +import EditorSafeRenderPlugin from './plugins/editor-safe-render/index.js'; import SwaggerUIAdapterPlugin from './plugins/swagger-ui-adapter/index.js'; -const SafeRenderPlugin = (system) => - SwaggerUI.plugins.SafeRender({ - componentList: [ - 'TopBar', - 'SwaggerEditorLayout', - 'Editor', - 'EditorTextarea', - 'EditorMonaco', - 'EditorPane', - 'EditorPaneBarTop', - 'EditorPreviewPane', - 'ValidationPane', - 'AlertDialog', - 'ConfirmDialog', - 'Dropzone', - ], - })(system); - const SwaggerEditor = React.memo((props) => { const mergedProps = deepmerge(SwaggerEditor.defaultProps, props); @@ -73,6 +56,7 @@ SwaggerEditor.plugins = { EditorPreviewSwaggerUI: EditorPreviewSwaggerUIPlugin, EditorPreviewAsyncAPI: EditorPreviewAsyncAPIPlugin, EditorPreviewApiDesignSystems: EditorPreviewApiDesignSystemsPlugin, + EditorSafeRender: EditorSafeRenderPlugin, TopBar: TopBarPlugin, SplashScreenPlugin, Layout: LayoutPlugin, @@ -98,7 +82,7 @@ SwaggerEditor.presets = { TopBarPlugin, SplashScreenPlugin, LayoutPlugin, - SafeRenderPlugin, + EditorSafeRenderPlugin, ], monaco: () => [ ModalsPlugin, @@ -121,7 +105,7 @@ SwaggerEditor.presets = { TopBarPlugin, SplashScreenPlugin, LayoutPlugin, - SafeRenderPlugin, + EditorSafeRenderPlugin, ], default: (...args) => SwaggerEditor.presets.monaco(...args), }; diff --git a/src/plugins/editor-safe-render/components/ErrorBoundary.jsx b/src/plugins/editor-safe-render/components/ErrorBoundary.jsx new file mode 100644 index 00000000000..ce932ec9261 --- /dev/null +++ b/src/plugins/editor-safe-render/components/ErrorBoundary.jsx @@ -0,0 +1,74 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; + +class ErrorBoundary extends Component { + static defaultState = { hasError: false, error: null, editorContent: null }; + + static getDerivedStateFromError(error) { + return { hasError: true, error }; + } + + constructor(...args) { + super(...args); + this.state = this.constructor.defaultState; + } + + componentDidMount() { + const { editorSelectors } = this.props; + + this.setState({ editorContent: editorSelectors.selectContent() }); + } + + componentDidUpdate(prevProps, prevState) { + const { editorSelectors } = this.props; + const hasEditorContentChanged = prevState.editorContent !== editorSelectors.selectContent(); + + if (!hasEditorContentChanged) return; + + const newState = { editorContent: editorSelectors.selectContent() }; + + if (prevState.hasError) { + newState.hasError = false; + newState.error = null; + } + + this.setState(newState); + } + + componentDidCatch(error, errorInfo) { + const { + fn: { componentDidCatch }, + } = this.props; + + componentDidCatch(error, errorInfo); + } + + render() { + const { hasError, error } = this.state; + const { getComponent, targetName, children } = this.props; + + if (hasError && error) { + const FallbackComponent = getComponent('Fallback'); + return ; + } + + return children; + } +} +ErrorBoundary.propTypes = { + targetName: PropTypes.string, + getComponent: PropTypes.func.isRequired, + fn: PropTypes.shape({ + componentDidCatch: PropTypes.func.isRequired, + }).isRequired, + editorSelectors: PropTypes.shape({ + selectContent: PropTypes.func.isRequired, + }).isRequired, + children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]), +}; +ErrorBoundary.defaultProps = { + targetName: 'this component', + children: null, +}; + +export default ErrorBoundary; diff --git a/src/plugins/editor-safe-render/index.js b/src/plugins/editor-safe-render/index.js new file mode 100644 index 00000000000..db1c77abd0c --- /dev/null +++ b/src/plugins/editor-safe-render/index.js @@ -0,0 +1,39 @@ +import SwaggerUI from 'swagger-ui-react'; + +import ErrorBoundaryWrapper from './wrap-components/ErrorBoundaryWrapper.jsx'; + +/** + * This is special version of SwaggerUI.plugins.SafeRender. + * In editor context, we want to dismiss the error produced + * in error boundary if editor content has changed. + */ +const EditorSafeRenderPlugin = () => { + const safeRenderPlugin = () => + SwaggerUI.plugins.SafeRender({ + fullOverride: true, + componentList: [ + 'TopBar', + 'SwaggerEditorLayout', + 'Editor', + 'EditorTextarea', + 'EditorMonaco', + 'EditorPane', + 'EditorPaneBarTop', + 'EditorPreviewPane', + 'ValidationPane', + 'AlertDialog', + 'ConfirmDialog', + 'Dropzone', + ], + }); + + const safeRenderPluginOverride = () => ({ + wrapComponents: { + ErrorBoundary: ErrorBoundaryWrapper, + }, + }); + + return [safeRenderPlugin, safeRenderPluginOverride]; +}; + +export default EditorSafeRenderPlugin; diff --git a/src/plugins/editor-safe-render/wrap-components/ErrorBoundaryWrapper.jsx b/src/plugins/editor-safe-render/wrap-components/ErrorBoundaryWrapper.jsx new file mode 100644 index 00000000000..f5482c1f8be --- /dev/null +++ b/src/plugins/editor-safe-render/wrap-components/ErrorBoundaryWrapper.jsx @@ -0,0 +1,16 @@ +import React from 'react'; + +import ErrorBoundary from '../components/ErrorBoundary.jsx'; + +const ErrorBoundaryWrapper = (Original, system) => { + const ErrorBoundaryOverride = (props) => { + const { editorSelectors } = system; + + // eslint-disable-next-line react/jsx-props-no-spreading + return ; + }; + + return ErrorBoundaryOverride; +}; + +export default ErrorBoundaryWrapper;