Files
medusa-store/packages/admin/dashboard/src/components/layout/shell/shell.tsx
Kasper Fabricius Kristensen 493d242c12 fix(dashboard): Add Breadcrumb components (#10079)
**What**
- Adds Breadcrumb component to all routes that needs breadcrumbs.
- The Breadcrumb components use a combination of loader data and useQuery to ensure that the displayed value is kept up to date if the underlying data is changed via a mutation.
- Also fixes a couple of places where the breadcrumb was not setup correctly.

Resolves CMRC-688
2024-11-15 13:13:03 +00:00

216 lines
6.0 KiB
TypeScript

import * as Dialog from "@radix-ui/react-dialog"
import { SidebarLeft, TriangleRightMini, XMark } from "@medusajs/icons"
import { IconButton, clx } from "@medusajs/ui"
import { PropsWithChildren, ReactNode } from "react"
import { useTranslation } from "react-i18next"
import { Link, Outlet, UIMatch, useMatches } from "react-router-dom"
import { KeybindProvider } from "../../../providers/keybind-provider"
import { useGlobalShortcuts } from "../../../providers/keybind-provider/hooks"
import { useSidebar } from "../../../providers/sidebar-provider"
import { Notifications } from "../notifications"
export const Shell = ({ children }: PropsWithChildren) => {
const globalShortcuts = useGlobalShortcuts()
return (
<KeybindProvider shortcuts={globalShortcuts}>
<div className="flex h-screen flex-col items-start overflow-hidden lg:flex-row">
<div>
<MobileSidebarContainer>{children}</MobileSidebarContainer>
<DesktopSidebarContainer>{children}</DesktopSidebarContainer>
</div>
<div className="flex h-screen w-full flex-col overflow-auto">
<Topbar />
<main className="flex h-full w-full flex-col items-center overflow-y-auto">
<Gutter>
<Outlet />
</Gutter>
</main>
</div>
</div>
</KeybindProvider>
)
}
const Gutter = ({ children }: PropsWithChildren) => {
return (
<div className="flex w-full max-w-[1600px] flex-col gap-y-2 p-3">
{children}
</div>
)
}
const Breadcrumbs = () => {
const matches = useMatches() as unknown as UIMatch<
unknown,
{
breadcrumb?: (match?: UIMatch) => string | ReactNode
}
>[]
const crumbs = matches
.filter((match) => match.handle?.breadcrumb)
.map((match) => {
const handle = match.handle
let label: string | ReactNode | undefined = undefined
try {
label = handle.breadcrumb?.(match)
} catch (error) {
// noop
}
if (!label) {
return null
}
return {
label: label,
path: match.pathname,
}
})
.filter(Boolean) as { label: string | ReactNode; path: string }[]
return (
<ol
className={clx(
"text-ui-fg-muted txt-compact-small-plus flex select-none items-center"
)}
>
{crumbs.map((crumb, index) => {
const isLast = index === crumbs.length - 1
const isSingle = crumbs.length === 1
return (
<li key={index} className={clx("flex items-center")}>
{!isLast ? (
<Link
className="transition-fg hover:text-ui-fg-subtle"
to={crumb.path}
>
{crumb.label}
</Link>
) : (
<div>
{!isSingle && <span className="block lg:hidden">...</span>}
<span
key={index}
className={clx({
"hidden lg:block": !isSingle,
})}
>
{crumb.label}
</span>
</div>
)}
{!isLast && (
<span className="mx-2">
<TriangleRightMini />
</span>
)}
</li>
)
})}
</ol>
)
}
const ToggleSidebar = () => {
const { toggle } = useSidebar()
return (
<div>
<IconButton
className="hidden lg:flex"
variant="transparent"
onClick={() => toggle("desktop")}
size="small"
>
<SidebarLeft className="text-ui-fg-muted" />
</IconButton>
<IconButton
className="hidden max-lg:flex"
variant="transparent"
onClick={() => toggle("mobile")}
size="small"
>
<SidebarLeft className="text-ui-fg-muted" />
</IconButton>
</div>
)
}
const Topbar = () => {
return (
<div className="grid w-full grid-cols-2 border-b p-3">
<div className="flex items-center gap-x-1.5">
<ToggleSidebar />
<Breadcrumbs />
</div>
<div className="flex items-center justify-end gap-x-3">
<Notifications />
</div>
</div>
)
}
const DesktopSidebarContainer = ({ children }: PropsWithChildren) => {
const { desktop } = useSidebar()
return (
<div
className={clx("hidden h-screen w-[220px] border-r", {
"lg:flex": desktop,
})}
>
{children}
</div>
)
}
const MobileSidebarContainer = ({ children }: PropsWithChildren) => {
const { t } = useTranslation()
const { mobile, toggle } = useSidebar()
return (
<Dialog.Root open={mobile} onOpenChange={() => toggle("mobile")}>
<Dialog.Portal>
<Dialog.Overlay
className={clx(
"bg-ui-bg-overlay fixed inset-0",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
)}
/>
<Dialog.Content
className={clx(
"bg-ui-bg-subtle shadow-elevation-modal fixed inset-y-2 left-2 flex w-full max-w-[304px] flex-col overflow-hidden rounded-lg border-r",
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:slide-out-to-left-1/2 data-[state=open]:slide-in-from-left-1/2 duration-200"
)}
>
<div className="p-3">
<Dialog.Close asChild>
<IconButton
size="small"
variant="transparent"
className="text-ui-fg-subtle"
>
<XMark />
</IconButton>
</Dialog.Close>
<Dialog.Title className="sr-only">
{t("app.nav.accessibility.title")}
</Dialog.Title>
<Dialog.Description className="sr-only">
{t("app.nav.accessibility.description")}
</Dialog.Description>
</div>
{children}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
)
}