From ec3d9f20a4fba3753afd8e9c80eb0fbdf3c0077d Mon Sep 17 00:00:00 2001 From: Ricky Smith Date: Tue, 28 Nov 2023 11:29:25 +0100 Subject: [PATCH] Refactor theming of ErrorBoundary Refactoring to a function-component is not possible, as there is no alternative to `componentDidCatch`, according to the [React Docs](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary): > There is currently no way to write an error boundary as a function component. --- .../src/error/errorboundary/ErrorBoundary.tsx | 239 ++++++++++++------ 1 file changed, 167 insertions(+), 72 deletions(-) diff --git a/packages/admin/admin/src/error/errorboundary/ErrorBoundary.tsx b/packages/admin/admin/src/error/errorboundary/ErrorBoundary.tsx index c112ee9c81..28b1082781 100644 --- a/packages/admin/admin/src/error/errorboundary/ErrorBoundary.tsx +++ b/packages/admin/admin/src/error/errorboundary/ErrorBoundary.tsx @@ -1,11 +1,11 @@ import { ChevronDown, ChevronRight, Error } from "@comet/admin-icons"; -import { Alert, AlertProps, ComponentsOverrides, Theme, Typography } from "@mui/material"; -import { WithStyles } from "@mui/styles"; -import createStyles from "@mui/styles/createStyles"; -import withStyles from "@mui/styles/withStyles"; +import { Alert as MuiAlert, AlertProps, ComponentsOverrides, Typography } from "@mui/material"; +import { css, styled, Theme, useThemeProps } from "@mui/material/styles"; import * as React from "react"; import { FormattedMessage } from "react-intl"; +import { ThemedComponentBaseProps } from "../../helpers/ThemedComponentBaseProps"; + export type ErrorBoundaryClassKey = | "alert" | "message" @@ -17,6 +17,10 @@ export type ErrorBoundaryClassKey = | "exceptionSummaryTitle" | "exceptionStackTrace"; +type OwnerState = { + showDetails?: boolean; +}; + export type ErrorBoundaryProps = React.PropsWithChildren<{ userErrorMessage?: React.ReactNode; variant?: AlertProps["variant"]; @@ -24,111 +28,202 @@ export type ErrorBoundaryProps = React.PropsWithChildren<{ toggleDetailsOpenedIcon?: React.ReactNode; toggleDetailsClosedIcon?: React.ReactNode; key?: string | number; -}>; +}> & + ThemedComponentBaseProps<{ + alert: typeof MuiAlert; + message: typeof Typography; + exceptionDetails: "details"; + exceptionSummary: "summary"; + exceptionSummaryIconOpened: "div"; + exceptionSummaryIconClosed: "div"; + exceptionSummaryTitle: typeof Typography; + exceptionStackTrace: typeof Typography; + }>; interface IErrorBoundaryState { error?: Error; errorInfo?: React.ErrorInfo; + showDetails?: boolean; } -class ErrorBoundary extends React.Component, IErrorBoundaryState> { - constructor(props: ErrorBoundaryProps & WithStyles) { +const Alert = styled(MuiAlert, { + name: "CometAdminErrorBoundary", + slot: "alert", + overridesResolver(_, styles) { + return [styles.alert]; + }, +})(); + +const Message = styled(Typography, { + name: "CometAdminErrorBoundary", + slot: "message", + overridesResolver(_, styles) { + return [styles.message]; + }, +})(); + +const ExceptionDetails = styled("details", { + name: "CometAdminErrorBoundary", + slot: "exceptionDetails", + overridesResolver(_, styles) { + return [styles.exceptionDetails]; + }, +})( + () => css` + white-space: pre-wrap; + `, +); + +const ExceptionSummary = styled("summary", { + name: "CometAdminErrorBoundary", + slot: "exceptionSummary", + overridesResolver(_, styles) { + return [styles.exceptionSummary]; + }, +})( + ({ theme }) => css` + display: flex; + align-items: center; + cursor: pointer; + outline: none; + padding-top: ${theme.spacing(2)}; + + &:first-of-type { + list-style-type: none; + } + `, +); + +const ExceptionSummaryIconOpen = styled("div", { + name: "CometAdminErrorBoundary", + slot: "exceptionSummaryIconOpened", + overridesResolver(_, styles) { + return [styles.exceptionSummaryIcon, styles.exceptionSummaryIconOpened]; + }, +})<{ ownerState: OwnerState }>( + ({ ownerState }) => css` + align-items: center; + display: flex; + + ${ownerState.showDetails && + css` + display: none; + `} + `, +); + +const ExceptionSummaryIconClosed = styled("div", { + name: "CometAdminErrorBoundary", + slot: "exceptionSummaryIconClosed", + overridesResolver(_, styles) { + return [styles.exceptionSummaryIcon, styles.exceptionSummaryIconClosed]; + }, +})<{ ownerState: OwnerState }>( + ({ ownerState }) => css` + align-items: center; + display: none; + + ${ownerState.showDetails && + css` + display: flex; + `} + `, +); + +const ExceptionSummaryTitle = styled(Typography, { + name: "CometAdminErrorBoundary", + slot: "exceptionSummaryTitle", + overridesResolver(_, styles) { + return [styles.exceptionSummaryTitle]; + }, +})( + ({ theme }) => css` + font-weight: ${theme.typography.fontWeightBold}; + padding-left: ${theme.spacing(1)}; + `, +); + +const ExceptionStackTrace = styled(Typography, { + name: "CometAdminErrorBoundary", + slot: "exceptionStackTrace", + overridesResolver(_, styles) { + return [styles.exceptionStackTrace]; + }, +})(); + +export const ErrorBoundary = (inProps: ErrorBoundaryProps) => { + const props = useThemeProps({ props: inProps, name: "CometAdminErrorBoundary" }); + return ; +}; + +class CoreErrorBoundary extends React.Component { + constructor(props: ErrorBoundaryProps) { super(props); this.state = {}; } public componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { - this.setState({ error, errorInfo }); + this.setState((prev) => ({ ...prev, error, errorInfo })); } public render() { const { - classes, variant = "filled", icon = , toggleDetailsOpenedIcon = , toggleDetailsClosedIcon = , + children, + slotProps, + userErrorMessage, + key, + ...restProps } = this.props; - const { error, errorInfo } = this.state; + const { error, errorInfo, showDetails } = this.state; + + const ownerState: OwnerState = { + showDetails, + }; if (errorInfo != null) { return ( - - - {this.props.userErrorMessage ? ( - this.props.userErrorMessage + + + {userErrorMessage ? ( + userErrorMessage ) : ( )} - + {process.env.NODE_ENV === "development" && ( -
- -
+ { + const showDetails = e.currentTarget.hasAttribute("open"); + this.setState((prev) => ({ ...prev, showDetails })); + }} + {...slotProps?.exceptionDetails} + > + + {toggleDetailsOpenedIcon} -
-
+ + {toggleDetailsClosedIcon} -
- {error != null && error.toString()} -
- - {errorInfo.componentStack} -
+ + + {error != null && error.toString()} + + + {errorInfo.componentStack}__ + )}
); } - return <>{this.props.children}; + return <>{children}; } } -const styles = (theme: Theme) => - createStyles({ - alert: {}, - message: {}, - exceptionDetails: { - whiteSpace: "pre-wrap", - "&[open]": { - "& $exceptionSummaryIconClosed": { - display: "flex", - }, - "& $exceptionSummaryIconOpened": { - display: "none", - }, - }, - }, - exceptionSummary: { - display: "flex", - alignItems: "center", - cursor: "pointer", - outline: "none", - paddingTop: theme.spacing(2), - "&:first-of-type ": { - listStyleType: "none", - }, - }, - exceptionSummaryIcon: { - alignItems: "center", - }, - exceptionSummaryIconOpened: { - display: "flex", - }, - exceptionSummaryIconClosed: { - display: "none", - }, - exceptionSummaryTitle: { - fontWeight: theme.typography.fontWeightBold, - paddingLeft: theme.spacing(1), - }, - exceptionStackTrace: {}, - }); - -const StyledErrorBoundary = withStyles(styles, { name: "CometAdminErrorBoundary" })(ErrorBoundary); - -export { StyledErrorBoundary as ErrorBoundary }; - declare module "@mui/material/styles" { interface ComponentNameToClassKey { CometAdminErrorBoundary: ErrorBoundaryClassKey;