@wordpress/editor
Version:
Enhanced block editor for WordPress posts.
210 lines (187 loc) • 5.68 kB
JavaScript
import { Component } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
// eslint-disable-next-line @wordpress/use-recommended-components -- The fallback UI renders outside the editor's notice system.
import { Card, CollapsibleCard, Notice, Stack, Text } from '@wordpress/ui';
import { select } from '@wordpress/data';
import { useCopyToClipboard } from '@wordpress/compose';
import { doAction } from '@wordpress/hooks';
import { store as editorStore } from '../../store';
function getContent() {
try {
// While `select` in a component is generally discouraged, it is
// used here because it (a) reduces the chance of data loss in the
// case of additional errors by performing a direct retrieval and
// (b) avoids the performance cost associated with unnecessary
// content serialization throughout the lifetime of a non-erroring
// application.
return select( editorStore ).getEditedPostContent();
} catch {}
}
// A boundary catches whatever was thrown, which is not always an `Error`.
function getErrorName( error ) {
return ( error instanceof Error && error.name ) || 'Error';
}
function getErrorMessage( error ) {
if ( typeof error === 'string' && error ) {
return error;
}
if ( typeof error?.message === 'string' && error.message ) {
return error.message;
}
return 'An unknown error occurred.';
}
// The sections of the report, shared by the copied Markdown and the details
// panel so that the two cannot drift apart. Deliberately untranslated: both
// are developer-facing, and the report is pasted into a bug report.
function getErrorSections( error, componentStack ) {
const sections = [
{ label: getErrorName( error ), content: getErrorMessage( error ) },
];
if ( error?.stack ) {
sections.push( {
label: 'Stack',
content: error.stack.trim(),
preformatted: true,
} );
}
if ( componentStack ) {
sections.push( {
label: 'Component stack',
content: componentStack.trim(),
preformatted: true,
} );
}
sections.push( {
label: 'Environment',
content: `User agent: ${ window.navigator.userAgent }`,
preformatted: true,
} );
return sections;
}
// Markdown, so the report stays readable as plain text and renders when pasted
// into a bug report.
function getErrorReport( error, componentStack ) {
const sections = getErrorSections( error, componentStack ).map(
( { label, content, preformatted } ) =>
`**${ label }**\n\n${
preformatted ? `\`\`\`\n${ content }\n\`\`\`` : content
}`
);
return [ '### Error report', ...sections ].join( '\n\n' );
}
function CopyButton( { text, children, variant = 'outline' } ) {
const ref = useCopyToClipboard( text );
return (
<Notice.ActionButton variant={ variant } ref={ ref }>
{ children }
</Notice.ActionButton>
);
}
function ErrorReport( { error, componentStack } ) {
return (
<Stack
className="editor-error-boundary__report"
direction="column"
gap="md"
>
{ getErrorSections( error, componentStack ).map(
( { label, content } ) => (
<Stack key={ label } direction="column" gap="xs">
<Text variant="heading-md">{ label }</Text>
<pre className="editor-error-boundary__report-section">
{ content }
</pre>
</Stack>
)
) }
</Stack>
);
}
function ErrorDetails( { error, componentStack } ) {
return (
<CollapsibleCard.Root className="editor-error-boundary__details">
<CollapsibleCard.Header>
<Card.Title>{ __( 'Error details' ) }</Card.Title>
</CollapsibleCard.Header>
<CollapsibleCard.Content>
<ErrorReport
error={ error }
componentStack={ componentStack }
/>
</CollapsibleCard.Content>
</CollapsibleCard.Root>
);
}
class ErrorBoundary extends Component {
constructor() {
super( ...arguments );
this.state = {
error: null,
componentStack: null,
};
}
componentDidCatch( error, errorInfo ) {
this.setState( { componentStack: errorInfo?.componentStack } );
doAction( 'editor.ErrorBoundary.errorLogged', error, errorInfo );
}
static getDerivedStateFromError( error ) {
return { error };
}
render() {
const { error, componentStack } = this.state;
const { canCopyContent = false } = this.props;
if ( ! error ) {
return this.props.children;
}
return (
<Stack
className="editor-error-boundary"
direction="column"
gap="lg"
>
<Notice.Root intent="error">
<Notice.Title>
{ __( 'The editor has crashed' ) }
</Notice.Title>
<Notice.Description>
{ __(
'An unknown error occurred. Reload your browser to try again, or copy the error to report the problem or search.'
) }
</Notice.Description>
<Notice.Actions>
{ canCopyContent && (
<CopyButton text={ getContent }>
{ __( 'Copy contents' ) }
</CopyButton>
) }
<CopyButton
variant="solid"
text={ () =>
getErrorReport( error, componentStack )
}
>
{ __( 'Copy error' ) }
</CopyButton>
</Notice.Actions>
</Notice.Root>
{ globalThis.SCRIPT_DEBUG ? (
<ErrorDetails
error={ error }
componentStack={ componentStack }
/>
) : null }
</Stack>
);
}
}
/**
* ErrorBoundary is used to catch JavaScript errors anywhere in a child component tree, log those errors, and display a fallback UI.
*
* It uses the lifecycle methods getDerivedStateFromError and componentDidCatch to catch errors in a child component tree.
*
* getDerivedStateFromError is used to render a fallback UI after an error has been thrown, and componentDidCatch is used to log error information.
*
* @class ErrorBoundary
* @augments Component
*/
export default ErrorBoundary;