**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)
378 lines
9.9 KiB
TypeScript
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>
|
|
)
|
|
}
|