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;