From 3a92956becf920700d6ca784418d0217f8a53b47 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 27 Oct 2017 11:59:30 -0400 Subject: [PATCH 1/6] Components: Enable ClipboardButton to generate text by callback --- components/clipboard-button/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/components/clipboard-button/index.js b/components/clipboard-button/index.js index 80a96920e5cbd1..d11d90e87ebdfe 100644 --- a/components/clipboard-button/index.js +++ b/components/clipboard-button/index.js @@ -57,7 +57,12 @@ class ClipboardButton extends Component { } getText() { - return this.props.text; + let text = this.props.text; + if ( 'function' === typeof text ) { + text = text(); + } + + return text; } render() { From 9c4f8bb2bdf997a7684e83c930109c9b02b1f9a4 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 27 Oct 2017 12:00:03 -0400 Subject: [PATCH 2/6] Components: Pass ClipboardButton extra props to Button --- components/clipboard-button/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/components/clipboard-button/index.js b/components/clipboard-button/index.js index d11d90e87ebdfe..77487acdf3d367 100644 --- a/components/clipboard-button/index.js +++ b/components/clipboard-button/index.js @@ -66,12 +66,14 @@ class ClipboardButton extends Component { } render() { - const { className, children } = this.props; + // Disable reason: Exclude from spread props passed to Button + // eslint-disable-next-line no-unused-vars + const { className, children, onCopy, text, ...buttonProps } = this.props; const classes = classnames( 'components-clipboard-button', className ); return (
-
From 29325a58379e024cde2067ddde5c308268226bd1 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 27 Oct 2017 12:00:18 -0400 Subject: [PATCH 3/6] Components: Render ClipboardButton as span to avoid layout implications --- components/clipboard-button/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/clipboard-button/index.js b/components/clipboard-button/index.js index 77487acdf3d367..3c4a78f30783a0 100644 --- a/components/clipboard-button/index.js +++ b/components/clipboard-button/index.js @@ -72,11 +72,11 @@ class ClipboardButton extends Component { const classes = classnames( 'components-clipboard-button', className ); return ( -
+ -
+ ); } } From bcf9e75bd9f6aebf352dfd8b2c320571cc716fcb Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Fri, 27 Oct 2017 12:01:00 -0400 Subject: [PATCH 4/6] Framework: Capture and recover from application error --- editor/assets/stylesheets/_z-index.scss | 2 +- editor/components/error-boundary/index.js | 70 +++++++++++++++++++ editor/components/index.js | 2 + editor/components/provider/index.js | 9 ++- .../warning/index.js} | 11 ++- editor/components/warning/style.scss | 29 ++++++++ editor/index.js | 40 +++++++++-- .../visual-editor/block-crash-warning.js | 6 +- .../visual-editor/invalid-block-warning.js | 6 +- editor/modes/visual-editor/style.scss | 36 +--------- editor/store.js | 7 +- element/index.js | 9 ++- 12 files changed, 169 insertions(+), 58 deletions(-) create mode 100644 editor/components/error-boundary/index.js rename editor/{modes/visual-editor/block-warning.js => components/warning/index.js} (51%) create mode 100644 editor/components/warning/style.scss diff --git a/editor/assets/stylesheets/_z-index.scss b/editor/assets/stylesheets/_z-index.scss index 629e37628f6e1f..db9a54cd2e76ca 100644 --- a/editor/assets/stylesheets/_z-index.scss +++ b/editor/assets/stylesheets/_z-index.scss @@ -8,7 +8,7 @@ $z-layers: ( '.editor-visual-editor__block .wp-block-more:before': -1, '.editor-visual-editor__block {core/image aligned left or right}': 20, '.freeform-toolbar': 10, - '.editor-visual-editor__block-warning': 1, + '.editor-warning': 1, '.editor-visual-editor__sibling-inserter': 1, '.components-form-toggle__input': 1, '.editor-format-list__menu': 1, diff --git a/editor/components/error-boundary/index.js b/editor/components/error-boundary/index.js new file mode 100644 index 00000000000000..36ed83d4e6046b --- /dev/null +++ b/editor/components/error-boundary/index.js @@ -0,0 +1,70 @@ +/** + * External dependencies + */ +import { noop } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { Button, ClipboardButton } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { Warning } from '../'; + +class ErrorBoundary extends Component { + constructor() { + super( ...arguments ); + + this.reboot = this.reboot.bind( this ); + this.getStateText = this.getStateText.bind( this ); + + this.state = { + hasEncounteredError: false, + }; + } + + componentDidCatch() { + this.setState( { hasEncounteredError: true } ); + } + + reboot() { + this.props.onError( this.context.store.getState() ); + } + + getStateText() { + return JSON.stringify( this.context.store.getState() ); + } + + render() { + const { hasEncounteredError } = this.state; + if ( ! hasEncounteredError ) { + return this.props.children; + } + + return ( + +

{ __( + 'The editor has encountered an unexpected error.' + ) }

+

+ + + { __( 'Copy State to Clipboard' ) } + +

+
+ ); + } +} + +ErrorBoundary.contextTypes = { + store: noop, +}; + +export default ErrorBoundary; diff --git a/editor/components/index.js b/editor/components/index.js index c1808ef6756fd6..adbb8903c8966b 100644 --- a/editor/components/index.js +++ b/editor/components/index.js @@ -35,7 +35,9 @@ export { default as WordCount } from './word-count'; // Content Related Components export { default as BlockInspector } from './block-inspector'; +export { default as ErrorBoundary } from './error-boundary'; export { default as Inserter } from './inserter'; +export { default as Warning } from './warning'; // State Related Components export { default as EditorProvider } from './provider'; diff --git a/editor/components/provider/index.js b/editor/components/provider/index.js index 7737be7a7bc3f5..b9b0b3e99816ca 100644 --- a/editor/components/provider/index.js +++ b/editor/components/provider/index.js @@ -42,8 +42,13 @@ class EditorProvider extends Component { constructor( props ) { super( ...arguments ); - const store = createReduxStore(); - store.dispatch( setupEditor( props.post ) ); + const store = createReduxStore( props.initialState ); + + // If initial state is passed, assume that we don't need to initialize, + // as in the case of an error recovery. + if ( ! props.initialState ) { + store.dispatch( setupEditor( props.post ) ); + } this.store = store; this.settings = { diff --git a/editor/modes/visual-editor/block-warning.js b/editor/components/warning/index.js similarity index 51% rename from editor/modes/visual-editor/block-warning.js rename to editor/components/warning/index.js index 598da141ffaca9..e099b947a63a4b 100644 --- a/editor/modes/visual-editor/block-warning.js +++ b/editor/components/warning/index.js @@ -3,13 +3,18 @@ */ import { Dashicon } from '@wordpress/components'; -function BlockWarning( { children } ) { +/** + * Internal dependencies + */ +import './style.scss'; + +function Warning( { children } ) { return ( -
+
{ children }
); } -export default BlockWarning; +export default Warning; diff --git a/editor/components/warning/style.scss b/editor/components/warning/style.scss new file mode 100644 index 00000000000000..4f9d7119ab0937 --- /dev/null +++ b/editor/components/warning/style.scss @@ -0,0 +1,29 @@ +.editor-warning { + z-index: z-index( '.editor-warning' ); + position: absolute; + top: 50%; + left: 50%; + transform: translate( -50%, -50% ); + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: center; + width: 96%; + max-width: 780px; + padding: 20px 20px 10px 20px; + background-color: $white; + border: 1px solid $light-gray-500; + text-align: center; + line-height: $default-line-height; + box-shadow: $shadow-popover; + + .editor-visual-editor & p { + width: 100%; + font-family: $default-font; + font-size: $default-font-size; + } + + .components-button { + margin: 0 #{ $item-spacing / 2 } 5px; + } +} diff --git a/editor/index.js b/editor/index.js index 5b9883d12c3af8..4091f164981cfa 100644 --- a/editor/index.js +++ b/editor/index.js @@ -7,7 +7,7 @@ import 'moment-timezone/moment-timezone-utils'; /** * WordPress dependencies */ -import { render } from '@wordpress/element'; +import { render, unmountComponentAtNode } from '@wordpress/element'; import { settings as dateSettings } from '@wordpress/date'; /** @@ -15,7 +15,7 @@ import { settings as dateSettings } from '@wordpress/date'; */ import './assets/stylesheets/main.scss'; import Layout from './layout'; -import { EditorProvider } from './components'; +import { EditorProvider, ErrorBoundary } from './components'; import { initializeMetaBoxState } from './actions'; export * from './components'; @@ -45,23 +45,49 @@ window.jQuery( document ).on( 'heartbeat-tick', ( event, response ) => { } } ); +/** + * Reinitializes the editor after the user chooses to reboot the editor after + * an unhandled error occurs, replacing previously mounted editor element using + * an initial state from prior to the crash. + * + * @param {Element} target DOM node in which editor is rendered + * @param {*} initialState Initial editor state to hydrate + */ +export function recreateEditorInstance( target, initialState ) { + unmountComponentAtNode( target ); + + const reboot = recreateEditorInstance.bind( null, target ); + + render( + + + + + , + target + ); +} + /** * Initializes and returns an instance of Editor. * * The return value of this function is not necessary if we change where we * call createEditorInstance(). This is due to metaBox timing. * - * @param {String} id Unique identifier for editor instance - * @param {Object} post API entity for post to edit - * @param {?Object} settings Editor settings object - * @return {Object} Editor interface. Currently supports metabox initialization. + * @param {String} id Unique identifier for editor instance + * @param {Object} post API entity for post to edit + * @param {?Object} settings Editor settings object + * @return {Object} Editor interface */ export function createEditorInstance( id, post, settings ) { const target = document.getElementById( id ); + const reboot = recreateEditorInstance.bind( null, target ); const provider = render( - + + + , target ); diff --git a/editor/modes/visual-editor/block-crash-warning.js b/editor/modes/visual-editor/block-crash-warning.js index b1834926026460..c2154507433159 100644 --- a/editor/modes/visual-editor/block-crash-warning.js +++ b/editor/modes/visual-editor/block-crash-warning.js @@ -6,14 +6,14 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import BlockWarning from './block-warning'; +import { Warning } from '../../components'; const warning = ( - +

{ __( 'This block has suffered from an unhandled error and cannot be previewed.' ) }

-
+ ); export default () => warning; diff --git a/editor/modes/visual-editor/invalid-block-warning.js b/editor/modes/visual-editor/invalid-block-warning.js index 26d580b07dabcc..2ac3b64a65046d 100644 --- a/editor/modes/visual-editor/invalid-block-warning.js +++ b/editor/modes/visual-editor/invalid-block-warning.js @@ -12,7 +12,7 @@ import { Button } from '@wordpress/components'; /** * Internal dependencies */ -import BlockWarning from './block-warning'; +import { Warning } from '../../components'; import { getBlockType, getUnknownTypeHandlerName, @@ -31,7 +31,7 @@ function InvalidBlockWarning( { ignoreInvalid, switchToBlockType } ) { const switchTo = ( blockType ) => () => switchToBlockType( blockType ); return ( - +

{ defaultBlockType && htmlBlockType && sprintf( __( 'This block appears to have been modified externally. ' + 'Overwrite the external changes or Convert to %s or %s to keep ' + @@ -67,7 +67,7 @@ function InvalidBlockWarning( { ignoreInvalid, switchToBlockType } ) { ) }

-
+ ); } diff --git a/editor/modes/visual-editor/style.scss b/editor/modes/visual-editor/style.scss index 29d6de04bbb28e..8dc799a27991e3 100644 --- a/editor/modes/visual-editor/style.scss +++ b/editor/modes/visual-editor/style.scss @@ -51,7 +51,7 @@ position: relative; min-height: 250px; - > :not( .editor-visual-editor__block-warning ) { + > :not( .editor-warning ) { pointer-events: none; user-select: none; } @@ -421,40 +421,6 @@ height: 20px; } -.editor-visual-editor__block-warning { - z-index: z-index( '.editor-visual-editor__block-warning' ); - position: absolute; - top: 50%; - left: 50%; - transform: translate( -50%, -50% ); - display: flex; - flex-direction: column; - justify-content: space-around; - align-items: center; - width: 96%; - max-width: 780px; - padding: 20px 20px 10px 20px; - background-color: $white; - border: 1px solid $light-gray-500; - text-align: center; - line-height: $default-line-height; - box-shadow: $shadow-popover; - - p { - width: 100%; - font-family: $default-font; - font-size: $default-font-size; - } - - .button + .button { - margin-left: $item-spacing; - } -} - -.visual-editor__invalid-block-warning-buttons .components-button { - margin-bottom: 5px; -} - .editor-visual-editor__block .blocks-visual-editor__block-html-textarea { display: block; margin: 0; diff --git a/editor/store.js b/editor/store.js index fa2e7db7dfc7c4..3dbc97fd5220c3 100644 --- a/editor/store.js +++ b/editor/store.js @@ -21,9 +21,10 @@ const GUTENBERG_PREFERENCES_KEY = `GUTENBERG_PREFERENCES_${ window.userSettings. /** * Creates a new instance of a Redux store. * - * @return {Redux.Store} Redux store + * @param {?*} preloadedState Optional initial state + * @return {Redux.Store} Redux store */ -function createReduxStore() { +function createReduxStore( preloadedState ) { const enhancers = [ applyMiddleware( multi, refx( effects ) ), storePersist( 'preferences', GUTENBERG_PREFERENCES_KEY ), @@ -33,7 +34,7 @@ function createReduxStore() { enhancers.push( window.__REDUX_DEVTOOLS_EXTENSION__() ); } - const store = createStore( reducer, flowRight( enhancers ) ); + const store = createStore( reducer, preloadedState, flowRight( enhancers ) ); return store; } diff --git a/element/index.js b/element/index.js index 97c8ef990fd63f..0826db19020dd6 100644 --- a/element/index.js +++ b/element/index.js @@ -2,7 +2,7 @@ * External dependencies */ import { createElement, Component, cloneElement, Children } from 'react'; -import { render, findDOMNode, createPortal } from 'react-dom'; +import { render, findDOMNode, createPortal, unmountComponentAtNode } from 'react-dom'; import { renderToStaticMarkup } from 'react-dom/server'; import { isString } from 'lodash'; @@ -27,6 +27,13 @@ export { createElement }; */ export { render }; +/** + * Removes any mounted element from the target DOM node. + * + * @param {Element} target DOM node in which element is to be removed + */ +export { unmountComponentAtNode }; + /** * A base class to create WordPress Components (Refs, state and lifecycle hooks) */ From a1d497e70bffb79ce7e333b5c258436f8463ae28 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 14 Nov 2017 10:17:22 -0500 Subject: [PATCH 5/6] Framework: Change copy options to post text, error stack --- editor/components/error-boundary/index.js | 26 ++++++++++++++--------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/editor/components/error-boundary/index.js b/editor/components/error-boundary/index.js index 36ed83d4e6046b..240674e5368b4c 100644 --- a/editor/components/error-boundary/index.js +++ b/editor/components/error-boundary/index.js @@ -14,34 +14,37 @@ import { Button, ClipboardButton } from '@wordpress/components'; * Internal dependencies */ import { Warning } from '../'; +import { getEditedPostContent } from '../../selectors'; class ErrorBoundary extends Component { constructor() { super( ...arguments ); this.reboot = this.reboot.bind( this ); - this.getStateText = this.getStateText.bind( this ); + this.getContent = this.getContent.bind( this ); this.state = { - hasEncounteredError: false, + error: null, }; } - componentDidCatch() { - this.setState( { hasEncounteredError: true } ); + componentDidCatch( error ) { + this.setState( { error } ); } reboot() { this.props.onError( this.context.store.getState() ); } - getStateText() { - return JSON.stringify( this.context.store.getState() ); + getContent() { + try { + return getEditedPostContent( this.context.store.getState() ); + } catch ( error ) {} } render() { - const { hasEncounteredError } = this.state; - if ( ! hasEncounteredError ) { + const { error } = this.state; + if ( ! error ) { return this.props.children; } @@ -54,8 +57,11 @@ class ErrorBoundary extends Component { - - { __( 'Copy State to Clipboard' ) } + + { __( 'Copy Post Text' ) } + + + { __( 'Copy Error' ) }

From cc658ae97834c504ca11e6401a5b88008a4ddacf Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 14 Nov 2017 10:18:36 -0500 Subject: [PATCH 6/6] Block: Reduce suffering of block errors Closes #3397 Consistency with new application-wide error capturing messaging. --- editor/modes/visual-editor/block-crash-warning.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/editor/modes/visual-editor/block-crash-warning.js b/editor/modes/visual-editor/block-crash-warning.js index c2154507433159..52dcab22852dd7 100644 --- a/editor/modes/visual-editor/block-crash-warning.js +++ b/editor/modes/visual-editor/block-crash-warning.js @@ -11,7 +11,7 @@ import { Warning } from '../../components'; const warning = (

{ __( - 'This block has suffered from an unhandled error and cannot be previewed.' + 'This block has encountered an error and cannot be previewed.' ) }

);