feat(admin-sdk,admin-bundler,admin-shared,medusa): Restructure admin packages (#8988)
**What** - Renames /admin-next -> /admin - Renames @medusajs/admin-sdk -> @medusajs/admin-bundler - Creates a new package called @medusajs/admin-sdk that will hold all tooling relevant to creating admin extensions. This is currently `defineRouteConfig` and `defineWidgetConfig`, but will eventually also export methods for adding custom fields, register translation, etc. - cc: @shahednasser we should update the examples in the docs so these functions are imported from `@medusajs/admin-sdk`. People will also need to install the package in their project, as it's no longer a transient dependency. - cc: @olivermrbl we might want to publish a changelog when this is merged, as it is a breaking change, and will require people to import the `defineXConfig` from the new package instead of `@medusajs/admin-shared`. - Updates CODEOWNERS so /admin packages does not require a review from the UI team.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export * from "./main-layout"
|
||||
@@ -0,0 +1,377 @@
|
||||
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 text-left">
|
||||
{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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user