feat(dashboard): Wrap each route in an ErrorBoundary (#8674)
**What** - Updates the copy of the different error types - Wraps each route (+ custom routes) in an ErrorBoundary to preserve app layout on error (sidebar and topbar)  **Note** If the user goes to a route that does not exist at all, e.g. `/some-weird-url`, then we have no way of knowing if the user is inside of a context where we can render the sidebar and topbar (as they require the user to be authenticated). So in this case we still show an ErrorBoundary where the two aren't included (see second picture), and include a button that takes the user to "/", which depending on whether the user is logged in will take them to "/login" or "/orders".  Resolves CC-248
This commit is contained in:
@@ -53,11 +53,24 @@ const Breadcrumbs = () => {
|
||||
.map((match) => {
|
||||
const handle = match.handle
|
||||
|
||||
let label: string | null = null
|
||||
|
||||
try {
|
||||
label = handle.crumb!(match.data)
|
||||
} catch (error) {
|
||||
// noop
|
||||
}
|
||||
|
||||
if (!label) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
label: handle.crumb!(match.data),
|
||||
label: label,
|
||||
path: match.pathname,
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as { label: string; path: string }[]
|
||||
|
||||
return (
|
||||
<ol
|
||||
|
||||
+16
-42
@@ -4,9 +4,7 @@ import { useTranslation } from "react-i18next"
|
||||
import { Navigate, useLocation, useRouteError } from "react-router-dom"
|
||||
|
||||
import { isFetchError } from "../../../lib/is-fetch-error"
|
||||
import { InlineTip } from "../../common/inline-tip"
|
||||
|
||||
// WIP - Need to allow wrapping <Outlet> with ErrorBoundary for more granular error handling.
|
||||
export const ErrorBoundary = () => {
|
||||
const error = useRouteError()
|
||||
const location = useLocation()
|
||||
@@ -45,47 +43,23 @@ export const ErrorBoundary = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex size-full min-h-screen items-center justify-center">
|
||||
<div className="text-ui-fg-subtle flex flex-col items-center gap-y-2">
|
||||
<ExclamationCircle />
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{title}
|
||||
</Text>
|
||||
<Text size="small" className="text-ui-fg-muted">
|
||||
{message}
|
||||
</Text>
|
||||
<DevelopmentStack error={error} />
|
||||
<div className="flex size-full min-h-[calc(100vh-57px-24px)] items-center justify-center">
|
||||
<div className="flex flex-col gap-y-6">
|
||||
<div className="text-ui-fg-subtle flex flex-col items-center gap-y-3">
|
||||
<ExclamationCircle />
|
||||
<div className="flex flex-col items-center justify-center gap-y-1">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{title}
|
||||
</Text>
|
||||
<Text
|
||||
size="small"
|
||||
className="text-ui-fg-muted text-balance text-center"
|
||||
>
|
||||
{message}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that renders an error stack trace in development mode.
|
||||
*
|
||||
* We don't want to show stack traces in production, so this component is only
|
||||
* rendered when the `NODE_ENV` is set to `development`.
|
||||
*
|
||||
* The reason for adding this is that `react-router-dom` can swallow certain types
|
||||
* of errors, e.g. a missing export from a module that is exported, and will instead
|
||||
* log a vague warning related to the route not exporting a Component.
|
||||
*/
|
||||
const DevelopmentStack = ({ error }: { error: unknown }) => {
|
||||
const stack = error instanceof Error ? error.stack : null
|
||||
const [stackType, stackMessage] = stack?.split(":") ?? ["", ""]
|
||||
const isDevelopment = process.env.NODE_ENV === "development"
|
||||
|
||||
if (!isDevelopment) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<InlineTip
|
||||
variant="warning"
|
||||
label={stackType}
|
||||
className="mx-auto w-full max-w-[500px]"
|
||||
>
|
||||
<span className="font-mono">{stackMessage}</span>
|
||||
</InlineTip>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -245,14 +245,16 @@
|
||||
"addFilter": "Add filter"
|
||||
},
|
||||
"errorBoundary": {
|
||||
"badRequestTitle": "Bad request",
|
||||
"badRequestMessage": "The request was invalid.",
|
||||
"notFoundTitle": "Not found",
|
||||
"notFoundMessage": "The page you are looking for does not exist.",
|
||||
"internalServerErrorTitle": "Internal server error",
|
||||
"internalServerErrorMessage": "An error occurred on the server.",
|
||||
"badRequestTitle": "400 - Bad request",
|
||||
"badRequestMessage": "The request could not be understood by the server due to malformed syntax.",
|
||||
"notFoundTitle": "404 - There is no page at this address",
|
||||
"notFoundMessage": "Check the URL and try again, or use the search bar to find what you are looking for.",
|
||||
"internalServerErrorTitle": "500 - Internal server error",
|
||||
"internalServerErrorMessage": "An unexpected error occurred on the server. Please try again later.",
|
||||
"defaultTitle": "An error occurred",
|
||||
"defaultMessage": "An error occurred while rendering this page."
|
||||
"defaultMessage": "An unexpected error occurred while rendering this page.",
|
||||
"noMatchMessage": "The page you are looking for does not exist.",
|
||||
"backToDashboard": "Back to dashboard"
|
||||
},
|
||||
"addresses": {
|
||||
"shippingAddress": {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { RouteObject } from "react-router-dom"
|
||||
import { ErrorBoundary } from "../components/utilities/error-boundary"
|
||||
|
||||
/**
|
||||
* Used to test if a route is a settings route.
|
||||
@@ -32,6 +33,7 @@ export const createRouteMap = (
|
||||
route.children ||= []
|
||||
route.children.push({
|
||||
path: "",
|
||||
ErrorBoundary: ErrorBoundary,
|
||||
async lazy() {
|
||||
return { Component }
|
||||
},
|
||||
|
||||
@@ -1 +1 @@
|
||||
export * from "./router-provider";
|
||||
export * from "./router-provider"
|
||||
|
||||
@@ -26,10 +26,6 @@ export const RouteMap: RouteObject[] = [
|
||||
path: "/login",
|
||||
lazy: () => import("../../routes/login"),
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
lazy: () => import("../../routes/home"),
|
||||
},
|
||||
{
|
||||
path: "*",
|
||||
lazy: () => import("../../routes/no-match"),
|
||||
@@ -46,8 +42,14 @@ export const RouteMap: RouteObject[] = [
|
||||
path: "/",
|
||||
element: <MainLayout />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
errorElement: <ErrorBoundary />,
|
||||
lazy: () => import("../../routes/home"),
|
||||
},
|
||||
{
|
||||
path: "/products",
|
||||
errorElement: <ErrorBoundary />,
|
||||
handle: {
|
||||
crumb: () => "Products",
|
||||
},
|
||||
@@ -158,6 +160,7 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "/categories",
|
||||
errorElement: <ErrorBoundary />,
|
||||
handle: {
|
||||
crumb: () => "Categories",
|
||||
},
|
||||
@@ -206,6 +209,7 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "/orders",
|
||||
errorElement: <ErrorBoundary />,
|
||||
handle: {
|
||||
crumb: () => "Orders",
|
||||
},
|
||||
@@ -264,6 +268,7 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "/promotions",
|
||||
errorElement: <ErrorBoundary />,
|
||||
handle: {
|
||||
crumb: () => "Promotions",
|
||||
},
|
||||
@@ -305,6 +310,7 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "/campaigns",
|
||||
errorElement: <ErrorBoundary />,
|
||||
handle: { crumb: () => "Campaigns" },
|
||||
children: [
|
||||
{
|
||||
@@ -341,6 +347,7 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "/collections",
|
||||
errorElement: <ErrorBoundary />,
|
||||
handle: {
|
||||
crumb: () => "Collections",
|
||||
},
|
||||
@@ -383,6 +390,7 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "/price-lists",
|
||||
errorElement: <ErrorBoundary />,
|
||||
handle: {
|
||||
crumb: () => "Price Lists",
|
||||
},
|
||||
@@ -434,6 +442,7 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "/customers",
|
||||
errorElement: <ErrorBoundary />,
|
||||
handle: {
|
||||
crumb: () => "Customers",
|
||||
},
|
||||
@@ -479,6 +488,7 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "/customer-groups",
|
||||
errorElement: <ErrorBoundary />,
|
||||
handle: {
|
||||
crumb: () => "Customer Groups",
|
||||
},
|
||||
@@ -534,6 +544,7 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "/reservations",
|
||||
errorElement: <ErrorBoundary />,
|
||||
handle: {
|
||||
crumb: () => "Reservations",
|
||||
},
|
||||
@@ -577,6 +588,7 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "/inventory",
|
||||
errorElement: <ErrorBoundary />,
|
||||
handle: {
|
||||
crumb: () => "Inventory",
|
||||
},
|
||||
@@ -660,10 +672,12 @@ export const RouteMap: RouteObject[] = [
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
errorElement: <ErrorBoundary />,
|
||||
lazy: () => import("../../routes/settings"),
|
||||
},
|
||||
{
|
||||
path: "profile",
|
||||
errorElement: <ErrorBoundary />,
|
||||
lazy: () => import("../../routes/profile/profile-detail"),
|
||||
handle: {
|
||||
crumb: () => "Profile",
|
||||
@@ -677,6 +691,7 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "regions",
|
||||
errorElement: <ErrorBoundary />,
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
crumb: () => "Regions",
|
||||
@@ -715,6 +730,7 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "store",
|
||||
errorElement: <ErrorBoundary />,
|
||||
lazy: () => import("../../routes/store/store-detail"),
|
||||
handle: {
|
||||
crumb: () => "Store",
|
||||
@@ -736,6 +752,7 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "users",
|
||||
errorElement: <ErrorBoundary />,
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
crumb: () => "Users",
|
||||
@@ -768,6 +785,7 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "sales-channels",
|
||||
errorElement: <ErrorBoundary />,
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
crumb: () => "Sales Channels",
|
||||
@@ -814,6 +832,7 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "locations",
|
||||
errorElement: <ErrorBoundary />,
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
crumb: () => "Locations & Shipping",
|
||||
@@ -865,6 +884,7 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "shipping-option-types",
|
||||
errorElement: <ErrorBoundary />,
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
crumb: () => "Shipping Option Types",
|
||||
@@ -962,6 +982,7 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "product-tags",
|
||||
errorElement: <ErrorBoundary />,
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
crumb: () => "Product Tags",
|
||||
@@ -999,6 +1020,7 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "workflows",
|
||||
errorElement: <ErrorBoundary />,
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
crumb: () => "Workflows",
|
||||
@@ -1031,6 +1053,7 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "product-types",
|
||||
errorElement: <ErrorBoundary />,
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
crumb: () => "Product Types",
|
||||
@@ -1069,6 +1092,7 @@ export const RouteMap: RouteObject[] = [
|
||||
|
||||
{
|
||||
path: "publishable-api-keys",
|
||||
errorElement: <ErrorBoundary />,
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
crumb: () => "Publishable API Keys",
|
||||
@@ -1128,6 +1152,7 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "secret-api-keys",
|
||||
errorElement: <ErrorBoundary />,
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
crumb: () => "Secret API Keys",
|
||||
@@ -1180,6 +1205,7 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "tax-regions",
|
||||
errorElement: <ErrorBoundary />,
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
crumb: () => "Tax Regions",
|
||||
@@ -1298,6 +1324,7 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
{
|
||||
path: "return-reasons",
|
||||
errorElement: <ErrorBoundary />,
|
||||
element: <Outlet />,
|
||||
handle: {
|
||||
crumb: () => "Return Reasons",
|
||||
|
||||
@@ -1,4 +1,36 @@
|
||||
import { ExclamationCircle } from "@medusajs/icons"
|
||||
import { Button, Text } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
// TODO: Add 404 page
|
||||
export const NoMatch = () => {
|
||||
return <div>404 Not Found</div>
|
||||
const { t } = useTranslation()
|
||||
|
||||
const title = t("errorBoundary.notFoundTitle")
|
||||
const message = t("errorBoundary.noMatchMessage")
|
||||
|
||||
return (
|
||||
<div className="flex size-full min-h-screen items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-y-6">
|
||||
<div className="text-ui-fg-subtle flex flex-col items-center gap-y-3">
|
||||
<ExclamationCircle />
|
||||
<div className="flex flex-col items-center justify-center gap-y-1">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{title}
|
||||
</Text>
|
||||
<Text
|
||||
size="small"
|
||||
className="text-ui-fg-muted text-balance text-center"
|
||||
>
|
||||
{message}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<Button asChild size="small" variant="secondary">
|
||||
<Link to="/">{t("errorBoundary.backToDashboard")}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user