Files
medusa-store/packages/admin-next/dashboard/src/components/layout/main-layout/main-layout.tsx
Kasper Fabricius Kristensen 75c5d5ad9e feat(dashboard,js-sdk,types): Update app layout, and add user sdk methods (#8182)
**What**
- Updates app layout (sidebar and topbar)
- Adds "System" option to theme toggle (we now default to system)
- Adds sdk methods for user endpoints (RESOLVES CC-67)
2024-07-19 11:18:48 +00:00

378 lines
9.9 KiB
TypeScript

import {
BuildingStorefront,
Buildings,
ChevronDownMini,
CogSixTooth,
CurrencyDollar,
EllipsisHorizontal,
MagnifyingGlass,
MinusMini,
OpenRectArrowOut,
ReceiptPercent,
ShoppingCart,
SquaresPlus,
Tag,
Users,
} from "@medusajs/icons"
import { Avatar, DropdownMenu, Text, clx } from "@medusajs/ui"
import * as Collapsible from "@radix-ui/react-collapsible"
import { useTranslation } from "react-i18next"
import { useStore } from "../../../hooks/api/store"
import { settingsRouteRegex } from "../../../lib/extension-helpers"
import { Divider } from "../../common/divider"
import { Skeleton } from "../../common/skeleton"
import { NavItem, NavItemProps } from "../../layout/nav-item"
import { Shell } from "../../layout/shell"
import { Link, useLocation, useNavigate } from "react-router-dom"
import routes from "virtual:medusa/routes/links"
import { useLogout } from "../../../hooks/api"
import { queryClient } from "../../../lib/query-client"
import { useSearch } from "../../../providers/search-provider"
import { UserMenu } from "../user-menu"
export const MainLayout = () => {
return (
<Shell>
<MainSidebar />
</Shell>
)
}
const MainSidebar = () => {
return (
<aside className="flex flex-1 flex-col justify-between overflow-y-auto">
<div className="flex flex-1 flex-col">
<div className="bg-ui-bg-subtle sticky top-0">
<Header />
<div className="px-3">
<Divider variant="dashed" />
</div>
</div>
<div className="flex flex-1 flex-col justify-between">
<div className="flex flex-1 flex-col">
<CoreRouteSection />
<ExtensionRouteSection />
</div>
<UtilitySection />
</div>
<div className="bg-ui-bg-subtle sticky bottom-0">
<UserSection />
</div>
</div>
</aside>
)
}
const Logout = () => {
const { t } = useTranslation()
const navigate = useNavigate()
const { mutateAsync: logoutMutation } = useLogout()
const handleLogout = async () => {
await logoutMutation(undefined, {
onSuccess: () => {
/**
* When the user logs out, we want to clear the query cache
*/
queryClient.clear()
navigate("/login")
},
})
}
return (
<DropdownMenu.Item onClick={handleLogout}>
<div className="flex items-center gap-x-2">
<OpenRectArrowOut className="text-ui-fg-subtle" />
<span>{t("app.menus.actions.logout")}</span>
</div>
</DropdownMenu.Item>
)
}
const Header = () => {
const { t } = useTranslation()
const { store, isPending, isError, error } = useStore()
const name = store?.name
const fallback = store?.name?.slice(0, 1).toUpperCase()
const isLoaded = !isPending && !!store && !!name && !!fallback
if (isError) {
throw error
}
return (
<div className="w-full p-3">
<DropdownMenu>
<DropdownMenu.Trigger
disabled={!isLoaded}
className={clx(
"bg-ui-bg-subtle transition-fg grid w-full grid-cols-[24px_1fr_15px] items-center gap-x-3 rounded-md p-0.5 pr-2 outline-none",
"hover:bg-ui-bg-subtle-hover",
"data-[state=open]:bg-ui-bg-subtle-hover",
"focus-visible:shadow-borders-focus"
)}
>
{fallback ? (
<Avatar variant="squared" size="xsmall" fallback={fallback} />
) : (
<Skeleton className="h-6 w-6 rounded-md" />
)}
<div className="block overflow-hidden">
{name ? (
<Text
size="small"
weight="plus"
leading="compact"
className="truncate"
>
{store.name}
</Text>
) : (
<Skeleton className="h-[9px] w-[120px]" />
)}
</div>
<EllipsisHorizontal className="text-ui-fg-muted" />
</DropdownMenu.Trigger>
{isLoaded && (
<DropdownMenu.Content className="w-[var(--radix-dropdown-menu-trigger-width)] min-w-0">
<div className="flex items-center gap-x-3 px-2 py-1">
<Avatar variant="squared" size="small" fallback={fallback} />
<div className="flex flex-col overflow-hidden">
<Text
size="small"
weight="plus"
leading="compact"
className="truncate"
>
{name}
</Text>
<Text
size="xsmall"
leading="compact"
className="text-ui-fg-subtle"
>
{t("app.nav.main.store")}
</Text>
</div>
</div>
<DropdownMenu.Separator />
<DropdownMenu.Item className="gap-x-2" asChild>
<Link to="/settings/store">
<BuildingStorefront className="text-ui-fg-subtle" />
{t("app.nav.main.storeSettings")}
</Link>
</DropdownMenu.Item>
<DropdownMenu.Separator />
<Logout />
</DropdownMenu.Content>
)}
</DropdownMenu>
</div>
)
}
const useCoreRoutes = (): Omit<NavItemProps, "pathname">[] => {
const { t } = useTranslation()
return [
{
icon: <ShoppingCart />,
label: t("orders.domain"),
to: "/orders",
items: [
// TODO: Enable when domin is introduced
// {
// 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",
},
// TODO: Enable when domin is introduced
// {
// label: t("giftCards.domain"),
// to: "/gift-cards",
// },
],
},
{
icon: <Buildings />,
label: t("inventory.domain"),
to: "/inventory",
items: [
{
label: t("reservations.domain"),
to: "/reservations",
},
],
},
{
icon: <Users />,
label: t("customers.domain"),
to: "/customers",
items: [
{
label: t("customerGroups.domain"),
to: "/customer-groups",
},
],
},
{
icon: <ReceiptPercent />,
label: t("promotions.domain"),
to: "/promotions",
items: [
{
label: t("campaigns.domain"),
to: "/campaigns",
},
],
},
{
icon: <CurrencyDollar />,
label: t("priceLists.domain"),
to: "/price-lists",
},
]
}
const Searchbar = () => {
const { t } = useTranslation()
const { toggleSearch } = useSearch()
return (
<div className="px-3">
<button
onClick={toggleSearch}
className={clx(
"bg-ui-bg-subtle text-ui-fg-subtle flex w-full items-center gap-x-2.5 rounded-md px-2 py-1 outline-none",
"hover:bg-ui-bg-subtle-hover",
"focus-visible:shadow-borders-focus"
)}
>
<MagnifyingGlass />
<div className="flex-1 text-left">
<Text size="small" leading="compact" weight="plus">
{t("app.search.label")}
</Text>
</div>
<Text size="small" leading="compact" className="text-ui-fg-muted">
K
</Text>
</button>
</div>
)
}
const CoreRouteSection = () => {
const coreRoutes = useCoreRoutes()
return (
<nav className="flex flex-col gap-y-1 py-3">
<Searchbar />
{coreRoutes.map((route) => {
return <NavItem key={route.to} {...route} />
})}
</nav>
)
}
const ExtensionRouteSection = () => {
const { t } = useTranslation()
const links = routes.links
const extensionLinks = links
.filter((link) => !settingsRouteRegex.test(link.path))
.sort((a, b) => a.label.localeCompare(b.label))
if (!extensionLinks.length) {
return null
}
return (
<div>
<div className="px-3">
<Divider variant="dashed" />
</div>
<div className="flex flex-col gap-y-1 py-3">
<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">
{t("app.nav.common.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>
<nav className="flex flex-col gap-y-0.5 py-1 pb-4">
{extensionLinks.map((link) => {
return (
<NavItem
key={link.path}
to={link.path}
label={link.label}
icon={link.icon ? <link.icon /> : <SquaresPlus />}
type="extension"
/>
)
})}
</nav>
</Collapsible.Content>
</Collapsible.Root>
</div>
</div>
)
}
const UtilitySection = () => {
const location = useLocation()
const { t } = useTranslation()
return (
<div className="flex flex-col gap-y-0.5 py-3">
<NavItem
label={t("app.nav.settings.header")}
to="/settings"
from={location.pathname}
icon={<CogSixTooth />}
/>
</div>
)
}
const UserSection = () => {
return (
<div>
<div className="px-3">
<Divider variant="dashed" />
</div>
<UserMenu />
</div>
)
}