MM-61734 Add withErrorBoundary HOC and use for PostComponent (#29801)
* MM-61734 Add withErrorBoundary HOC and use for PostComponent I originally planned to add error boundaries to some individual parts of the post, but it seems sufficient to add them to just the outer PostComponent. * Update button label
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
ce29be0f38
Коммит
c7eb908e38
@@ -49,6 +49,7 @@ import {getDateForUnixTicks, makeIsEligibleForClick} from 'utils/utils';
|
||||
|
||||
import type {PostActionComponent, PostPluginComponent} from 'types/store/plugins';
|
||||
|
||||
import {withPostErrorBoundary} from './post_error_boundary';
|
||||
import PostOptions from './post_options';
|
||||
import PostUserProfile from './user_profile';
|
||||
|
||||
@@ -119,7 +120,7 @@ export type Props = {
|
||||
pluginActions: PostActionComponent[];
|
||||
};
|
||||
|
||||
const PostComponent = (props: Props): JSX.Element => {
|
||||
function PostComponent(props: Props) {
|
||||
const {post, shouldHighlight, togglePostMenu} = props;
|
||||
|
||||
const isSearchResultItem = (props.matches && props.matches.length > 0) || props.isMentionSearch || (props.term && props.term.length > 0);
|
||||
@@ -670,6 +671,6 @@ const PostComponent = (props: Props): JSX.Element => {
|
||||
</PostAriaLabelDiv>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default PostComponent;
|
||||
export default withPostErrorBoundary(PostComponent);
|
||||
|
||||
43
webapp/channels/src/components/post/post_error_boundary.tsx
Обычный файл
43
webapp/channels/src/components/post/post_error_boundary.tsx
Обычный файл
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import type {FallbackProps} from 'components/with_error_boundary';
|
||||
import withErrorBoundary from 'components/with_error_boundary';
|
||||
|
||||
export function withPostErrorBoundary<P>(component: React.ComponentType<P>) {
|
||||
return withErrorBoundary<P>(component, {
|
||||
renderFallback: ({clearError}) => {
|
||||
return (
|
||||
<div className='a11y__section post'>
|
||||
<FormattedMessage
|
||||
id='post.renderError.message'
|
||||
defaultMessage='An error occurred while rendering this post.'
|
||||
tagName='p'
|
||||
/>
|
||||
<br/>
|
||||
<RetryButton clearError={clearError}/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function RetryButton({clearError}: FallbackProps) {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<button
|
||||
className='btn btn-tertiary'
|
||||
aria-label={intl.formatMessage({id: 'post.renderError.retryLabel', defaultMessage: 'Retry rendering this post'})}
|
||||
onClick={clearError}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='post.renderError.retry'
|
||||
defaultMessage='Retry'
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -43,7 +43,7 @@ exports[`components/post_view/post_list_row should have class hideAnimation for
|
||||
exports[`components/post_view/post_list_row should render channel intro message 1`] = `<Connect(ChannelIntroMessage) />`;
|
||||
|
||||
exports[`components/post_view/post_list_row should render combined post 1`] = `
|
||||
<Connect(Connect(PostComponent))
|
||||
<Connect(Connect(ErrorBoundary(PostComponent)))
|
||||
combinedId="user-activity-1234-5678"
|
||||
isLastPost={false}
|
||||
location="CENTER"
|
||||
@@ -101,7 +101,7 @@ exports[`components/post_view/post_list_row should render new messages line 1`]
|
||||
`;
|
||||
|
||||
exports[`components/post_view/post_list_row should render post 1`] = `
|
||||
<Connect(PostComponent)
|
||||
<Connect(ErrorBoundary(PostComponent))
|
||||
isLastPost={false}
|
||||
location="CENTER"
|
||||
post={
|
||||
|
||||
138
webapp/channels/src/components/with_error_boundary/index.test.tsx
Обычный файл
138
webapp/channels/src/components/with_error_boundary/index.test.tsx
Обычный файл
@@ -0,0 +1,138 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import {getChannelsInCategoryOrder} from 'selectors/views/channel_sidebar';
|
||||
|
||||
import {render, screen} from 'tests/react_testing_utils';
|
||||
|
||||
import type {FallbackProps} from '.';
|
||||
import withErrorBoundary from '.';
|
||||
|
||||
function renderFallbackWithRetry({clearError}: FallbackProps) {
|
||||
return (
|
||||
<div>
|
||||
<p>{'A rendering error occurred'}</p>
|
||||
<button onClick={clearError}>{'Try again?'}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
describe('withErrorBoundary', () => {
|
||||
const origError = console.error;
|
||||
beforeAll(() => {
|
||||
console.error = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
console.error = origError;
|
||||
});
|
||||
|
||||
test('should render the component normally', () => {
|
||||
function TestComponent() {
|
||||
return <span>{'TestComponent'}</span>;
|
||||
}
|
||||
const WrappedTestComponent = withErrorBoundary(TestComponent, {
|
||||
renderFallback: renderFallbackWithRetry,
|
||||
});
|
||||
|
||||
render(
|
||||
<WrappedTestComponent/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('TestComponent')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should render fallback when an error occurs during rendering', () => {
|
||||
function TestComponent(): JSX.Element {
|
||||
const obj = {} as any;
|
||||
|
||||
return <span>{'TestComponent' + obj.someField.thatDoesnt.exist.toString()}</span>;
|
||||
}
|
||||
const WrappedTestComponent = withErrorBoundary(TestComponent, {
|
||||
renderFallback: renderFallbackWithRetry,
|
||||
});
|
||||
|
||||
render(
|
||||
<WrappedTestComponent/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('A rendering error occurred')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should render fallback when an error occurs in a hook', () => {
|
||||
function useAnErrorForSomeReason(): string {
|
||||
throw new Error('hook error');
|
||||
}
|
||||
function TestComponent(): JSX.Element {
|
||||
const extraText = useAnErrorForSomeReason();
|
||||
|
||||
return <span>{'TestComponent' + extraText}</span>;
|
||||
}
|
||||
const WrappedTestComponent = withErrorBoundary(TestComponent, {
|
||||
renderFallback: renderFallbackWithRetry,
|
||||
});
|
||||
|
||||
render(
|
||||
<WrappedTestComponent/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('A rendering error occurred')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should render fallback when an error occurs in a selector', () => {
|
||||
function TestComponent(): JSX.Element {
|
||||
const extraText = useSelector(getChannelsInCategoryOrder);
|
||||
|
||||
return <span>{'TestComponent' + extraText}</span>;
|
||||
}
|
||||
const WrappedTestComponent = withErrorBoundary(TestComponent, {
|
||||
renderFallback: renderFallbackWithRetry,
|
||||
});
|
||||
|
||||
render(
|
||||
<WrappedTestComponent/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('A rendering error occurred')).toBeVisible();
|
||||
});
|
||||
|
||||
test('the user should be able to retry rendering the component', () => {
|
||||
let throwError = true;
|
||||
|
||||
function TestComponent(): JSX.Element {
|
||||
let obj: any;
|
||||
if (throwError) {
|
||||
obj = {};
|
||||
} else {
|
||||
obj = {
|
||||
someField: {
|
||||
thatDoesnt: {
|
||||
exist: [1, 2, 3],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return <span>{'TestComponent ' + obj.someField.thatDoesnt.exist.toString()}</span>;
|
||||
}
|
||||
const WrappedTestComponent = withErrorBoundary(TestComponent, {
|
||||
renderFallback: renderFallbackWithRetry,
|
||||
});
|
||||
|
||||
render(
|
||||
<WrappedTestComponent/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('A rendering error occurred')).toBeVisible();
|
||||
expect(screen.queryByText('TestComponent 1,2,3')).toBeNull();
|
||||
|
||||
throwError = false;
|
||||
|
||||
screen.getByText('Try again?').click();
|
||||
|
||||
expect(screen.queryByText('A rendering error occurred')).toBeNull();
|
||||
expect(screen.queryByText('TestComponent 1,2,3')).toBeVisible();
|
||||
});
|
||||
});
|
||||
54
webapp/channels/src/components/with_error_boundary/index.tsx
Обычный файл
54
webapp/channels/src/components/with_error_boundary/index.tsx
Обычный файл
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
type ErrorBoundaryState = {
|
||||
hasError: boolean;
|
||||
}
|
||||
|
||||
export type FallbackProps = {
|
||||
clearError: (e: React.MouseEvent) => void;
|
||||
};
|
||||
|
||||
type ErrorBoundaryOptions = {
|
||||
renderFallback: (props: FallbackProps) => React.ReactNode;
|
||||
};
|
||||
|
||||
export default function withErrorBoundary<P>(component: React.ComponentType<P>, options: ErrorBoundaryOptions) {
|
||||
const Component = component;
|
||||
const displayName = component.displayName ?? component.name ?? 'Component';
|
||||
|
||||
const WrappedComponent = class WrappedComponent extends React.PureComponent<P, ErrorBoundaryState> {
|
||||
static displayName = `ErrorBoundary(${displayName})`;
|
||||
|
||||
state = {
|
||||
hasError: false,
|
||||
};
|
||||
|
||||
static getDerivedStateFromError() {
|
||||
return {
|
||||
hasError: true,
|
||||
};
|
||||
}
|
||||
|
||||
clearError = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.setState({hasError: false});
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return options.renderFallback({
|
||||
clearError: this.clearError,
|
||||
});
|
||||
}
|
||||
|
||||
return <Component {...this.props}/>;
|
||||
}
|
||||
};
|
||||
|
||||
return WrappedComponent;
|
||||
}
|
||||
@@ -4731,6 +4731,9 @@
|
||||
"post.ariaLabel.replyMessage": "At {time} {date}, {authorName} replied, {message}",
|
||||
"post.reminder.acknowledgement": "You will be reminded at {reminderTime}, {reminderDate} about this message from {username}: {permaLink}",
|
||||
"post.reminder.systemBot": "Hi there, here's your reminder about this message from {username}: {permaLink}",
|
||||
"post.renderError.message": "An error occurred while rendering this post.",
|
||||
"post.renderError.retry": "Retry",
|
||||
"post.renderError.retryLabel": "Retry rendering this post",
|
||||
"postlist.toast.history": "Viewing message history",
|
||||
"postlist.toast.newMessages": "{count, number} new {count, plural, one {message} other {messages}}",
|
||||
"postlist.toast.newMessagesSince": "{count, number} new {count, plural, one {message} other {messages}} {isToday, select, true {} other {since}} {date}",
|
||||
|
||||
Ссылка в новой задаче
Block a user