Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Site Editor: Add error boundary #33921

Merged
merged 4 commits into from
Aug 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 82 additions & 79 deletions packages/edit-site/src/components/editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,15 @@ import NavigationSidebar from '../navigation-sidebar';
import URLQueryController from '../url-query-controller';
import InserterSidebar from '../secondary-sidebar/inserter-sidebar';
import ListViewSidebar from '../secondary-sidebar/list-view-sidebar';
import ErrorBoundary from '../error-boundary';
import { store as editSiteStore } from '../../store';

const interfaceLabels = {
secondarySidebar: __( 'Block Library' ),
drawer: __( 'Navigation Sidebar' ),
};

function Editor( { initialSettings } ) {
function Editor( { initialSettings, onError } ) {
const {
isInserterOpen,
isListViewOpen,
Expand Down Expand Up @@ -179,8 +180,6 @@ function Editor( { initialSettings } ) {
return (
<>
<URLQueryController />
<FullscreenMode isActive />
<UnsavedChangesWarning />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if moving these two components has any impact?
Other than that, this is looking good.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested, and I wasn't able to spot any.

The is-full-screen class is applied to the body, and an "unsaved changes warning" is triggered if I try to reload the page.

<SlotFillProvider>
<EntityProvider kind="root" type="site">
<EntityProvider
Expand All @@ -201,85 +200,89 @@ function Editor( { initialSettings } ) {
settings.__experimentalGlobalStylesBaseStyles
}
>
<KeyboardShortcuts.Register />
<SidebarComplementaryAreaFills />
<InterfaceSkeleton
labels={ interfaceLabels }
drawer={ <NavigationSidebar /> }
secondarySidebar={ secondarySidebar() }
sidebar={
sidebarIsOpened && (
<ComplementaryArea.Slot scope="core/edit-site" />
)
}
header={
<Header
openEntitiesSavedStates={
openEntitiesSavedStates
}
/>
}
notices={ <EditorSnackbars /> }
content={
<>
<EditorNotices />
{ template && (
<BlockEditor
setIsInserterOpen={
setIsInserterOpened
}
/>
) }
{ templateResolved &&
! template &&
settings?.siteUrl &&
entityId && (
<Notice
status="warning"
isDismissible={
false
<ErrorBoundary onError={ onError }>
<FullscreenMode isActive />
<UnsavedChangesWarning />
<KeyboardShortcuts.Register />
<SidebarComplementaryAreaFills />
<InterfaceSkeleton
labels={ interfaceLabels }
drawer={ <NavigationSidebar /> }
secondarySidebar={ secondarySidebar() }
sidebar={
sidebarIsOpened && (
<ComplementaryArea.Slot scope="core/edit-site" />
)
}
header={
<Header
openEntitiesSavedStates={
openEntitiesSavedStates
}
/>
}
notices={ <EditorSnackbars /> }
content={
<>
<EditorNotices />
{ template && (
<BlockEditor
setIsInserterOpen={
setIsInserterOpened
}
>
{ __(
"You attempted to edit an item that doesn't exist. Perhaps it was deleted?"
) }
</Notice>
/>
) }
<KeyboardShortcuts />
</>
}
actions={
<>
{ isEntitiesSavedStatesOpen ? (
<EntitiesSavedStates
close={
closeEntitiesSavedStates
}
/>
) : (
<div className="edit-site-editor__toggle-save-panel">
<Button
variant="secondary"
className="edit-site-editor__toggle-save-panel-button"
onClick={
openEntitiesSavedStates
{ templateResolved &&
! template &&
settings?.siteUrl &&
entityId && (
<Notice
status="warning"
isDismissible={
false
}
>
{ __(
"You attempted to edit an item that doesn't exist. Perhaps it was deleted?"
) }
</Notice>
) }
<KeyboardShortcuts />
</>
}
actions={
<>
{ isEntitiesSavedStatesOpen ? (
<EntitiesSavedStates
close={
closeEntitiesSavedStates
}
aria-expanded={
false
}
>
{ __(
'Open save panel'
) }
</Button>
</div>
) }
</>
}
footer={ <BlockBreadcrumb /> }
/>
<Popover.Slot />
<PluginArea />
/>
) : (
<div className="edit-site-editor__toggle-save-panel">
<Button
variant="secondary"
className="edit-site-editor__toggle-save-panel-button"
onClick={
openEntitiesSavedStates
}
aria-expanded={
false
}
>
{ __(
'Open save panel'
) }
</Button>
</div>
) }
</>
}
footer={ <BlockBreadcrumb /> }
/>
<Popover.Slot />
<PluginArea />
</ErrorBoundary>
</GlobalStylesProvider>
</BlockContextProvider>
</EntityProvider>
Expand Down
64 changes: 64 additions & 0 deletions packages/edit-site/src/components/error-boundary/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* WordPress dependencies
*/
import { Component } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { Button } from '@wordpress/components';
import { Warning } from '@wordpress/block-editor';
import { useCopyToClipboard } from '@wordpress/compose';

function CopyButton( { text, children } ) {
const ref = useCopyToClipboard( text );
return (
<Button variant="secondary" ref={ ref }>
{ children }
</Button>
);
}

export default class ErrorBoundary extends Component {
constructor() {
super( ...arguments );

this.reboot = this.reboot.bind( this );

this.state = {
error: null,
};
}

static getDerivedStateFromError( error ) {
return { error };
}
Comment on lines +30 to +32
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that other ErrorBoundary components use componentDidCatch for fallback rendering while React documentation recommends using getDerivedStateFromError.

Is there a special reason we want to use the componentDidCatch lifecycle method?


reboot() {
this.props.onError();
}

render() {
const { error } = this.state;
if ( ! error ) {
return this.props.children;
}

return (
<Warning
className="editor-error-boundary"
actions={ [
<Button
key="recovery"
onClick={ this.reboot }
variant="secondary"
>
{ __( 'Attempt Recovery' ) }
</Button>,
<CopyButton key="copy-error" text={ error.stack }>
{ __( 'Copy Error' ) }
</CopyButton>,
] }
>
{ __( 'The editor has encountered an unexpected error.' ) }
</Warning>
);
}
}
26 changes: 23 additions & 3 deletions packages/edit-site/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
registerCoreBlocks,
__experimentalRegisterExperimentalCoreBlocks,
} from '@wordpress/block-library';
import { render } from '@wordpress/element';
import { render, unmountComponentAtNode } from '@wordpress/element';
import { __experimentalFetchLinkSuggestions as fetchLinkSuggestions } from '@wordpress/core-data';

/**
Expand All @@ -16,6 +16,23 @@ import './hooks';
import './store';
import Editor from './components/editor';

/**
* 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 {?Object} settings Editor settings object.
*/
export function reinitializeEditor( target, settings ) {
unmountComponentAtNode( target );
const reboot = reinitializeEditor.bind( null, target, settings );
render(
<Editor initialSettings={ settings } onError={ reboot } />,
target
);
}

/**
* Initializes the site editor screen.
*
Expand All @@ -27,6 +44,9 @@ export function initialize( id, settings ) {
fetchLinkSuggestions( search, searchOptions, settings );
settings.__experimentalSpotlightEntityBlocks = [ 'core/template-part' ];

const target = document.getElementById( id );
const reboot = reinitializeEditor.bind( null, target, settings );

registerCoreBlocks();
if ( process.env.GUTENBERG_PHASE === 2 ) {
__experimentalRegisterExperimentalCoreBlocks( {
Expand All @@ -35,8 +55,8 @@ export function initialize( id, settings ) {
}

render(
<Editor initialSettings={ settings } />,
document.getElementById( id )
<Editor initialSettings={ settings } onError={ reboot } />,
target
);
}

Expand Down