**What** - Move packages for `next` version of admin to core repo **Other** - Since this PR introduces packages that depend on Vite 5, it also introduces @types/node@^20. We have never had a direct dependency on the types package for Node, and as far as I can see that has resulted in us using the types from Node.js@8, as those are a dependency of one of our dependencies. With the introduction of @types/node@^20, two of our packages had TS errors because they were using the NodeJS.Timer type, which was deprecated in Node.js@14. We should add specific @types/node packages to all our packages, but I haven't done so in this PR to keep it as clean as possible. - Q: @olivermrbl I've added the new packages to the ignore list for changeset, is this enough to prevent them from being published?
381 lines
11 KiB
TypeScript
381 lines
11 KiB
TypeScript
import {
|
|
ArrowRightOnRectangle,
|
|
BookOpen,
|
|
BuildingStorefront,
|
|
Calendar,
|
|
ChevronDownMini,
|
|
CircleHalfSolid,
|
|
CogSixTooth,
|
|
CurrencyDollar,
|
|
EllipsisHorizontal,
|
|
MinusMini,
|
|
ReceiptPercent,
|
|
ShoppingCart,
|
|
Sidebar,
|
|
SquaresPlus,
|
|
Tag,
|
|
Users,
|
|
} from "@medusajs/icons"
|
|
import { Avatar, DropdownMenu, IconButton, Text } from "@medusajs/ui"
|
|
import * as Collapsible from "@radix-ui/react-collapsible"
|
|
import * as Dialog from "@radix-ui/react-dialog"
|
|
import { useAdminDeleteSession, useAdminStore } from "medusa-react"
|
|
import { Link, useLocation, useNavigate } from "react-router-dom"
|
|
|
|
import { useAuth } from "../../../providers/auth-provider"
|
|
import { useTheme } from "../../../providers/theme-provider"
|
|
|
|
import { Fragment, useEffect, useState } from "react"
|
|
import { Breadcrumbs } from "./breadcrumbs"
|
|
import { NavItem, NavItemProps } from "./nav-item"
|
|
import { Notifications } from "./notifications"
|
|
import { SearchToggle } from "./search-toggle"
|
|
import { Spacer } from "./spacer"
|
|
|
|
import extensions from "medusa-admin:routes/links"
|
|
import { useTranslation } from "react-i18next"
|
|
|
|
export const MainNav = () => {
|
|
return (
|
|
<Fragment>
|
|
<DesktopNav />
|
|
<MobileNav />
|
|
</Fragment>
|
|
)
|
|
}
|
|
|
|
const MobileNav = () => {
|
|
const [open, setOpen] = useState(false)
|
|
const location = useLocation()
|
|
|
|
// If the user navigates to a new route, we want to close the menu
|
|
useEffect(() => {
|
|
setOpen(false)
|
|
}, [location.pathname])
|
|
|
|
return (
|
|
<div className="bg-ui-bg-base border-ui-border-base flex h-[57px] w-full items-center justify-between border-b px-4 md:hidden">
|
|
<Dialog.Root open={open} onOpenChange={setOpen}>
|
|
<div className="flex items-center gap-x-2">
|
|
<Dialog.Trigger asChild>
|
|
<IconButton variant="transparent">
|
|
<Sidebar />
|
|
</IconButton>
|
|
</Dialog.Trigger>
|
|
<Breadcrumbs />
|
|
</div>
|
|
<Dialog.Portal>
|
|
<Dialog.Overlay className="bg-ui-bg-overlay fixed inset-0 lg:hidden" />
|
|
<Dialog.Content className="bg-ui-bg-subtle fixed inset-y-0 left-0 flex w-full flex-col overflow-y-auto sm:max-w-[240px] lg:hidden">
|
|
<div className="flex flex-1 flex-col">
|
|
<div className="sticky top-0">
|
|
<Header />
|
|
<Spacer />
|
|
</div>
|
|
<CoreRouteSection />
|
|
<ExtensionRouteSection />
|
|
</div>
|
|
<div className="sticky bottom-0 flex w-full flex-col">
|
|
<SettingsSection />
|
|
<Spacer />
|
|
<UserSection />
|
|
</div>
|
|
</Dialog.Content>
|
|
</Dialog.Portal>
|
|
</Dialog.Root>
|
|
<div className="flex items-center gap-x-2">
|
|
<SearchToggle />
|
|
<Notifications />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const DesktopNav = () => {
|
|
return (
|
|
<aside className="flex h-full max-h-screen w-full max-w-[240px] flex-col justify-between overflow-y-auto max-md:hidden">
|
|
<div className="flex flex-1 flex-col">
|
|
<div className="bg-ui-bg-subtle sticky top-0">
|
|
<Header />
|
|
<Spacer />
|
|
</div>
|
|
<CoreRouteSection />
|
|
<ExtensionRouteSection />
|
|
</div>
|
|
<div className="bg-ui-bg-subtle sticky bottom-0 flex flex-col">
|
|
<SettingsSection />
|
|
<Spacer />
|
|
<UserSection />
|
|
</div>
|
|
</aside>
|
|
)
|
|
}
|
|
|
|
const Header = () => {
|
|
const { store } = useAdminStore()
|
|
const { setTheme, theme } = useTheme()
|
|
const { mutateAsync: logoutMutation } = useAdminDeleteSession()
|
|
const navigate = useNavigate()
|
|
|
|
const logout = async () => {
|
|
await logoutMutation(undefined, {
|
|
onSuccess: () => {
|
|
navigate("/login")
|
|
},
|
|
})
|
|
}
|
|
|
|
if (!store) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div className="w-full p-4">
|
|
<DropdownMenu>
|
|
<DropdownMenu.Trigger className="hover:bg-ui-bg-subtle-hover active:bg-ui-bg-subtle-pressed focus:bg-ui-bg-subtle-pressed transition-fg w-full rounded-md outline-none">
|
|
<div className="flex items-center justify-between p-1 md:pr-2">
|
|
<div className="flex items-center gap-x-3">
|
|
<div className="bg-ui-bg-base shadow-borders-base flex h-8 w-8 items-center justify-center overflow-hidden rounded-md">
|
|
<div className="bg-ui-bg-component flex h-[28px] w-[28px] items-center justify-center overflow-hidden rounded-[4px]">
|
|
{store.name[0].toUpperCase()}
|
|
</div>
|
|
</div>
|
|
<Text size="small" weight="plus" leading="compact">
|
|
{store.name}
|
|
</Text>
|
|
</div>
|
|
<div className="text-ui-fg-subtle">
|
|
<EllipsisHorizontal />
|
|
</div>
|
|
</div>
|
|
</DropdownMenu.Trigger>
|
|
<DropdownMenu.Content>
|
|
<DropdownMenu.Item>
|
|
<BuildingStorefront className="text-ui-fg-subtle mr-2" />
|
|
Store Settings
|
|
</DropdownMenu.Item>
|
|
<DropdownMenu.Separator />
|
|
<Link to="https://docs.medusajs.com/user-guide" target="_blank">
|
|
<DropdownMenu.Item>
|
|
<BookOpen className="text-ui-fg-subtle mr-2" />
|
|
Documentation
|
|
</DropdownMenu.Item>
|
|
</Link>
|
|
<Link to="https://medusajs.com/changelog/" target="_blank">
|
|
<DropdownMenu.Item>
|
|
<Calendar className="text-ui-fg-subtle mr-2" />
|
|
Changelog
|
|
</DropdownMenu.Item>
|
|
</Link>
|
|
<DropdownMenu.Separator />
|
|
<DropdownMenu.SubMenu>
|
|
<DropdownMenu.SubMenuTrigger className="rounded-md">
|
|
<CircleHalfSolid className="text-ui-fg-subtle mr-2" />
|
|
Theme
|
|
</DropdownMenu.SubMenuTrigger>
|
|
<DropdownMenu.SubMenuContent>
|
|
<DropdownMenu.RadioGroup value={theme}>
|
|
<DropdownMenu.RadioItem
|
|
value="light"
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
setTheme("light")
|
|
}}
|
|
>
|
|
Light
|
|
</DropdownMenu.RadioItem>
|
|
<DropdownMenu.RadioItem
|
|
value="dark"
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
setTheme("dark")
|
|
}}
|
|
>
|
|
Dark
|
|
</DropdownMenu.RadioItem>
|
|
</DropdownMenu.RadioGroup>
|
|
</DropdownMenu.SubMenuContent>
|
|
</DropdownMenu.SubMenu>
|
|
<DropdownMenu.Separator />
|
|
<DropdownMenu.Item onClick={logout}>
|
|
<ArrowRightOnRectangle className="text-ui-fg-subtle mr-2" />
|
|
Logout
|
|
<DropdownMenu.Shortcut>⌥⇧Q</DropdownMenu.Shortcut>
|
|
</DropdownMenu.Item>
|
|
</DropdownMenu.Content>
|
|
</DropdownMenu>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const useCoreRoutes = (): Omit<NavItemProps, "pathname">[] => {
|
|
const { t } = useTranslation()
|
|
|
|
return [
|
|
{
|
|
icon: <ShoppingCart />,
|
|
label: t("orders.domain"),
|
|
to: "/orders",
|
|
items: [
|
|
{
|
|
label: t("draftOrders.domain"),
|
|
to: "/draft-orders",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
icon: <Tag />,
|
|
label: t("products.domain"),
|
|
to: "/products",
|
|
items: [
|
|
{
|
|
label: t("collections.domain"),
|
|
to: "/collections",
|
|
},
|
|
{
|
|
label: t("categories.domain"),
|
|
to: "/categories",
|
|
},
|
|
{
|
|
label: t("giftCards.domain"),
|
|
to: "/gift-cards",
|
|
},
|
|
{
|
|
label: t("inventory.domain"),
|
|
to: "/inventory",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
icon: <Users />,
|
|
label: t("customers.domain"),
|
|
to: "/customers",
|
|
items: [
|
|
{
|
|
label: t("customerGroups.domain"),
|
|
to: "/customer-groups",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
icon: <ReceiptPercent />,
|
|
label: t("discounts.domain"),
|
|
to: "/discounts",
|
|
},
|
|
{
|
|
icon: <CurrencyDollar />,
|
|
label: t("pricing.domain"),
|
|
to: "/pricing",
|
|
},
|
|
]
|
|
}
|
|
|
|
const CoreRouteSection = () => {
|
|
const coreRoutes = useCoreRoutes()
|
|
|
|
return (
|
|
<nav className="flex flex-col gap-y-1 py-4">
|
|
{coreRoutes.map((route) => {
|
|
return <NavItem key={route.to} {...route} />
|
|
})}
|
|
</nav>
|
|
)
|
|
}
|
|
|
|
const ExtensionRouteSection = () => {
|
|
if (!extensions.links || extensions.links.length === 0) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<Spacer />
|
|
<div className="flex flex-col gap-y-4 py-4">
|
|
<Collapsible.Root defaultOpen>
|
|
<div className="px-4">
|
|
<Collapsible.Trigger asChild className="group/trigger">
|
|
<button className="text-ui-fg-subtle flex w-full items-center justify-between px-2">
|
|
<Text size="xsmall" weight="plus" leading="compact">
|
|
Extensions
|
|
</Text>
|
|
<div className="text-ui-fg-muted">
|
|
<ChevronDownMini className="group-data-[state=open]/trigger:hidden" />
|
|
<MinusMini className="group-data-[state=closed]/trigger:hidden" />
|
|
</div>
|
|
</button>
|
|
</Collapsible.Trigger>
|
|
</div>
|
|
<Collapsible.Content>
|
|
<div className="flex flex-col gap-y-1 py-1 pb-4">
|
|
{extensions.links.map((link) => {
|
|
return (
|
|
<NavItem
|
|
key={link.path}
|
|
to={link.path}
|
|
label={link.label}
|
|
icon={link.icon ? <link.icon /> : <SquaresPlus />}
|
|
type="extension"
|
|
/>
|
|
)
|
|
})}
|
|
</div>
|
|
</Collapsible.Content>
|
|
</Collapsible.Root>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const SettingsSection = () => {
|
|
return (
|
|
<div className="py-4">
|
|
<NavItem icon={<CogSixTooth />} label="Settings" to="/settings" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const UserSection = () => {
|
|
const { user } = useAuth()
|
|
|
|
if (!user) {
|
|
return null
|
|
}
|
|
|
|
const fallback =
|
|
user.first_name && user.last_name
|
|
? `${user.first_name[0]}${user.last_name[0]}`
|
|
: user.first_name
|
|
? user.first_name[0]
|
|
: user.email[0]
|
|
|
|
return (
|
|
<div className="p-4">
|
|
<Link
|
|
to="/settings/profile"
|
|
className="hover:bg-ui-bg-subtle-hover transition-fg active:bg-ui-bg-subtle-pressed focus:bg-ui-bg-subtle-pressed flex items-center gap-x-3 rounded-md p-1 outline-none"
|
|
>
|
|
<Avatar fallback={fallback.toUpperCase()} />
|
|
<div className="flex flex-1 flex-col">
|
|
{(user.first_name || user.last_name) && (
|
|
<Text
|
|
size="xsmall"
|
|
weight="plus"
|
|
leading="compact"
|
|
className="max-w-[90%] truncate"
|
|
>{`${user.first_name && `${user.first_name} `}${
|
|
user.last_name
|
|
}`}</Text>
|
|
)}
|
|
<Text
|
|
size="xsmall"
|
|
leading="compact"
|
|
className="text-ui-fg-subtle max-w-[90%] truncate"
|
|
>
|
|
{user.email}
|
|
</Text>
|
|
</div>
|
|
</Link>
|
|
</div>
|
|
)
|
|
}
|