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:
1 parent
b4d8e265e3
commit
3706bf51af
7 files changed
+106
-56
No files matched your search
@@ -53,11 +53,24 @@ const Breadcrumbs = () => {
|
|||||||
.map((match) => {
|
.map((match) => {
|
||||||
const handle = match.handle
|
const handle = match.handle
|
||||||
|
|
||||||
|
let label: string | null = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
label = handle.crumb!(match.data)
|
||||||
|
} catch (error) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!label) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: handle.crumb!(match.data),
|
label: label,
|
||||||
path: match.pathname,
|
path: match.pathname,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.filter(Boolean) as { label: string; path: string }[]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ol
|
<ol
|
||||||
|
|||||||
+16
-42
@@ -4,9 +4,7 @@ import { useTranslation } from "react-i18next"
|
|||||||
import { Navigate, useLocation, useRouteError } from "react-router-dom"
|
import { Navigate, useLocation, useRouteError } from "react-router-dom"
|
||||||
|
|
||||||
import { isFetchError } from "../../../lib/is-fetch-error"
|
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 = () => {
|
export const ErrorBoundary = () => {
|
||||||
const error = useRouteError()
|
const error = useRouteError()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
@@ -45,47 +43,23 @@ export const ErrorBoundary = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex size-full min-h-screen items-center justify-center">
|
<div className="flex size-full min-h-[calc(100vh-57px-24px)] items-center justify-center">
|
||||||
<div className="text-ui-fg-subtle flex flex-col items-center gap-y-2">
|
<div className="flex flex-col gap-y-6">
|
||||||
<ExclamationCircle />
|
<div className="text-ui-fg-subtle flex flex-col items-center gap-y-3">
|
||||||
<Text size="small" leading="compact" weight="plus">
|
<ExclamationCircle />
|
||||||
{title}
|
<div className="flex flex-col items-center justify-center gap-y-1">
|
||||||
</Text>
|
<Text size="small" leading="compact" weight="plus">
|
||||||
<Text size="small" className="text-ui-fg-muted">
|
{title}
|
||||||
{message}
|
</Text>
|
||||||
</Text>
|
<Text
|
||||||
<DevelopmentStack error={error} />
|
size="small"
|
||||||
|
className="text-ui-fg-muted text-balance text-center"
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</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"
|
"addFilter": "Add filter"
|
||||||
},
|
},
|
||||||
"errorBoundary": {
|
"errorBoundary": {
|
||||||
"badRequestTitle": "Bad request",
|
"badRequestTitle": "400 - Bad request",
|
||||||
"badRequestMessage": "The request was invalid.",
|
"badRequestMessage": "The request could not be understood by the server due to malformed syntax.",
|
||||||
"notFoundTitle": "Not found",
|
"notFoundTitle": "404 - There is no page at this address",
|
||||||
"notFoundMessage": "The page you are looking for does not exist.",
|
"notFoundMessage": "Check the URL and try again, or use the search bar to find what you are looking for.",
|
||||||
"internalServerErrorTitle": "Internal server error",
|
"internalServerErrorTitle": "500 - Internal server error",
|
||||||
"internalServerErrorMessage": "An error occurred on the server.",
|
"internalServerErrorMessage": "An unexpected error occurred on the server. Please try again later.",
|
||||||
"defaultTitle": "An error occurred",
|
"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": {
|
"addresses": {
|
||||||
"shippingAddress": {
|
"shippingAddress": {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { RouteObject } from "react-router-dom"
|
import { RouteObject } from "react-router-dom"
|
||||||
|
import { ErrorBoundary } from "../components/utilities/error-boundary"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to test if a route is a settings route.
|
* Used to test if a route is a settings route.
|
||||||
@@ -32,6 +33,7 @@ export const createRouteMap = (
|
|||||||
route.children ||= []
|
route.children ||= []
|
||||||
route.children.push({
|
route.children.push({
|
||||||
path: "",
|
path: "",
|
||||||
|
ErrorBoundary: ErrorBoundary,
|
||||||
async lazy() {
|
async lazy() {
|
||||||
return { Component }
|
return { Component }
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export * from "./router-provider";
|
export * from "./router-provider"
|
||||||
@@ -26,10 +26,6 @@ export const RouteMap: RouteObject[] = [
|
|||||||
path: "/login",
|
path: "/login",
|
||||||
lazy: () => import("../../routes/login"),
|
lazy: () => import("../../routes/login"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
lazy: () => import("../../routes/home"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "*",
|
path: "*",
|
||||||
lazy: () => import("../../routes/no-match"),
|
lazy: () => import("../../routes/no-match"),
|
||||||
@@ -46,8 +42,14 @@ export const RouteMap: RouteObject[] = [
|
|||||||
path: "/",
|
path: "/",
|
||||||
element: <MainLayout />,
|
element: <MainLayout />,
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
|
lazy: () => import("../../routes/home"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/products",
|
path: "/products",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Products",
|
crumb: () => "Products",
|
||||||
},
|
},
|
||||||
@@ -158,6 +160,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/categories",
|
path: "/categories",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Categories",
|
crumb: () => "Categories",
|
||||||
},
|
},
|
||||||
@@ -206,6 +209,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/orders",
|
path: "/orders",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Orders",
|
crumb: () => "Orders",
|
||||||
},
|
},
|
||||||
@@ -264,6 +268,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/promotions",
|
path: "/promotions",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Promotions",
|
crumb: () => "Promotions",
|
||||||
},
|
},
|
||||||
@@ -305,6 +310,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/campaigns",
|
path: "/campaigns",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
handle: { crumb: () => "Campaigns" },
|
handle: { crumb: () => "Campaigns" },
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
@@ -341,6 +347,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/collections",
|
path: "/collections",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Collections",
|
crumb: () => "Collections",
|
||||||
},
|
},
|
||||||
@@ -383,6 +390,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/price-lists",
|
path: "/price-lists",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Price Lists",
|
crumb: () => "Price Lists",
|
||||||
},
|
},
|
||||||
@@ -434,6 +442,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/customers",
|
path: "/customers",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Customers",
|
crumb: () => "Customers",
|
||||||
},
|
},
|
||||||
@@ -479,6 +488,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/customer-groups",
|
path: "/customer-groups",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Customer Groups",
|
crumb: () => "Customer Groups",
|
||||||
},
|
},
|
||||||
@@ -534,6 +544,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/reservations",
|
path: "/reservations",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Reservations",
|
crumb: () => "Reservations",
|
||||||
},
|
},
|
||||||
@@ -577,6 +588,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/inventory",
|
path: "/inventory",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Inventory",
|
crumb: () => "Inventory",
|
||||||
},
|
},
|
||||||
@@ -660,10 +672,12 @@ export const RouteMap: RouteObject[] = [
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
index: true,
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
lazy: () => import("../../routes/settings"),
|
lazy: () => import("../../routes/settings"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "profile",
|
path: "profile",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
lazy: () => import("../../routes/profile/profile-detail"),
|
lazy: () => import("../../routes/profile/profile-detail"),
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Profile",
|
crumb: () => "Profile",
|
||||||
@@ -677,6 +691,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "regions",
|
path: "regions",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
element: <Outlet />,
|
element: <Outlet />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Regions",
|
crumb: () => "Regions",
|
||||||
@@ -715,6 +730,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "store",
|
path: "store",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
lazy: () => import("../../routes/store/store-detail"),
|
lazy: () => import("../../routes/store/store-detail"),
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Store",
|
crumb: () => "Store",
|
||||||
@@ -736,6 +752,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "users",
|
path: "users",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
element: <Outlet />,
|
element: <Outlet />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Users",
|
crumb: () => "Users",
|
||||||
@@ -768,6 +785,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "sales-channels",
|
path: "sales-channels",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
element: <Outlet />,
|
element: <Outlet />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Sales Channels",
|
crumb: () => "Sales Channels",
|
||||||
@@ -814,6 +832,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "locations",
|
path: "locations",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
element: <Outlet />,
|
element: <Outlet />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Locations & Shipping",
|
crumb: () => "Locations & Shipping",
|
||||||
@@ -865,6 +884,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "shipping-option-types",
|
path: "shipping-option-types",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
element: <Outlet />,
|
element: <Outlet />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Shipping Option Types",
|
crumb: () => "Shipping Option Types",
|
||||||
@@ -962,6 +982,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "product-tags",
|
path: "product-tags",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
element: <Outlet />,
|
element: <Outlet />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Product Tags",
|
crumb: () => "Product Tags",
|
||||||
@@ -999,6 +1020,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "workflows",
|
path: "workflows",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
element: <Outlet />,
|
element: <Outlet />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Workflows",
|
crumb: () => "Workflows",
|
||||||
@@ -1031,6 +1053,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "product-types",
|
path: "product-types",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
element: <Outlet />,
|
element: <Outlet />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Product Types",
|
crumb: () => "Product Types",
|
||||||
@@ -1069,6 +1092,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
path: "publishable-api-keys",
|
path: "publishable-api-keys",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
element: <Outlet />,
|
element: <Outlet />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Publishable API Keys",
|
crumb: () => "Publishable API Keys",
|
||||||
@@ -1128,6 +1152,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "secret-api-keys",
|
path: "secret-api-keys",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
element: <Outlet />,
|
element: <Outlet />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Secret API Keys",
|
crumb: () => "Secret API Keys",
|
||||||
@@ -1180,6 +1205,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "tax-regions",
|
path: "tax-regions",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
element: <Outlet />,
|
element: <Outlet />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Tax Regions",
|
crumb: () => "Tax Regions",
|
||||||
@@ -1298,6 +1324,7 @@ export const RouteMap: RouteObject[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "return-reasons",
|
path: "return-reasons",
|
||||||
|
errorElement: <ErrorBoundary />,
|
||||||
element: <Outlet />,
|
element: <Outlet />,
|
||||||
handle: {
|
handle: {
|
||||||
crumb: () => "Return Reasons",
|
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
|
// TODO: Add 404 page
|
||||||
export const NoMatch = () => {
|
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