feat(dashboard,admin-sdk,admin-shared,admin-vite-plugin): Add support for UI extensions (#7383)

* intial work

* update lock

* add routes and fix HMR of configs

* cleanup

* rm imports

* rm debug from plugin

* address feedback

* address feedback
This commit is contained in:
Kasper Fabricius Kristensen
2024-05-23 14:02:19 +02:00
committed by GitHub
parent 521c252dee
commit f1176a0673
50 changed files with 1366 additions and 1098 deletions

View File

@@ -13,12 +13,15 @@ import { Avatar, Text } from "@medusajs/ui"
import * as Collapsible from "@radix-ui/react-collapsible"
import { useTranslation } from "react-i18next"
import { ComponentType } from "react"
import { useStore } from "../../../hooks/api/store"
import { Skeleton } from "../../common/skeleton"
import { NavItem, NavItemProps } from "../../layout/nav-item"
import { Shell } from "../../layout/shell"
import routes from "virtual:medusa/routes/links"
import { settingsRouteRegex } from "../../../lib/extension-helpers"
import { Divider } from "../../common/divider"
export const MainLayout = () => {
return (
<Shell>
@@ -34,7 +37,7 @@ const MainSidebar = () => {
<div className="bg-ui-bg-subtle sticky top-0">
<Header />
<div className="px-3">
<div className="border-ui-border-strong h-px w-full border-b border-dashed" />
<Divider variant="dashed" />
</div>
</div>
<CoreRouteSection />
@@ -157,7 +160,7 @@ const CoreRouteSection = () => {
const coreRoutes = useCoreRoutes()
return (
<nav className="flex flex-col gap-y-1 py-2">
<nav className="flex flex-col gap-y-1 py-3">
{coreRoutes.map((route) => {
return <NavItem key={route.to} {...route} />
})}
@@ -165,27 +168,31 @@ const CoreRouteSection = () => {
)
}
const extensions = {
links: null as { path: string; label: string; icon?: ComponentType }[] | null,
}
const ExtensionRouteSection = () => {
if (!extensions.links || extensions.links.length === 0) {
const { t } = useTranslation()
const links = routes.links
const extensionLinks = links.filter(
(link) => !settingsRouteRegex.test(link.path)
)
if (!extensionLinks.length) {
return null
}
return (
<div>
<div className="px-3">
<div className="border-ui-border-strong h-px w-full border-b border-dashed" />
<Divider variant="dashed" />
</div>
<div className="flex flex-col gap-y-1 py-2">
<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">
Extensions
{t("nav.extensions")}
</Text>
<div className="text-ui-fg-muted">
<ChevronDownMini className="group-data-[state=open]/trigger:hidden" />
@@ -196,7 +203,7 @@ const ExtensionRouteSection = () => {
</div>
<Collapsible.Content>
<div className="flex flex-col gap-y-1 py-1 pb-4">
{extensions.links.map((link) => {
{extensionLinks.map((link) => {
return (
<NavItem
key={link.path}

View File

@@ -1,6 +1,6 @@
import { Text, clx } from "@medusajs/ui"
import * as Collapsible from "@radix-ui/react-collapsible"
import { useEffect, useState } from "react"
import { ReactNode, useEffect, useState } from "react"
import { Link, useLocation } from "react-router-dom"
type ItemType = "core" | "extension"
@@ -11,7 +11,7 @@ type NestedItemProps = {
}
export type NavItemProps = {
icon?: React.ReactNode
icon?: ReactNode
label: string
to: string
items?: NestedItemProps[]
@@ -55,7 +55,7 @@ export const NavItem = ({
: undefined
}
className={clx(
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover flex items-center gap-x-2 rounded-md px-2 py-2.5 outline-none md:py-1.5",
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover flex items-center gap-x-2 rounded-md px-2 py-1 outline-none",
{
"bg-ui-bg-base hover:bg-ui-bg-base-hover shadow-elevation-card-rest":
location.pathname === to ||
@@ -82,7 +82,7 @@ export const NavItem = ({
</Text>
</Collapsible.Trigger>
<Collapsible.Content className="flex flex-col pt-1">
<div className="flex h-[36px] w-full items-center gap-x-1 pl-2 md:hidden">
<div className="flex w-full items-center gap-x-1 pl-2 md:hidden">
<div
role="presentation"
className="flex h-full w-5 items-center justify-center"
@@ -92,7 +92,7 @@ export const NavItem = ({
<Link
to={to}
className={clx(
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover mb-2 mt-1 flex h-8 flex-1 items-center gap-x-2 rounded-md px-2 py-2.5 outline-none md:py-1.5",
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover mb-2 mt-1 flex flex-1 items-center gap-x-2 rounded-md px-2 py-1 outline-none",
{
"bg-ui-bg-base hover:bg-ui-bg-base text-ui-fg-base shadow-elevation-card-rest":
location.pathname.startsWith(to),
@@ -109,7 +109,7 @@ export const NavItem = ({
return (
<li
key={item.to}
className="flex h-[36px] items-center gap-x-1 pl-2"
className="flex h-[32px] items-center gap-x-1 pl-2"
>
<div
role="presentation"
@@ -120,7 +120,7 @@ export const NavItem = ({
<Link
to={item.to}
className={clx(
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover flex h-8 flex-1 items-center gap-x-2 rounded-md px-2 py-2.5 outline-none first-of-type:mt-1 last-of-type:mb-2 md:py-1.5",
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover flex flex-1 items-center gap-x-2 rounded-md px-2 py-1 outline-none first-of-type:mt-1 last-of-type:mb-2",
{
"bg-ui-bg-base text-ui-fg-base hover:bg-ui-bg-base shadow-elevation-card-rest":
location.pathname.startsWith(item.to),
@@ -142,7 +142,7 @@ export const NavItem = ({
)
}
const Icon = ({ icon, type }: { icon?: React.ReactNode; type: ItemType }) => {
const Icon = ({ icon, type }: { icon?: ReactNode; type: ItemType }) => {
if (!icon) {
return null
}

View File

@@ -1,13 +1,17 @@
import { ArrowUturnLeft, MinusMini } from "@medusajs/icons"
import { IconButton, Text } from "@medusajs/ui"
import * as Collapsible from "@radix-ui/react-collapsible"
import { useEffect, useMemo, useState } from "react"
import { Fragment, useEffect, useMemo, useState } from "react"
import { useTranslation } from "react-i18next"
import { Link, useLocation } from "react-router-dom"
import { settingsRouteRegex } from "../../../lib/extension-helpers"
import { Divider } from "../../common/divider"
import { NavItem, NavItemProps } from "../nav-item"
import { Shell } from "../shell"
import routes from "virtual:medusa/routes/links"
export const SettingsLayout = () => {
return (
<Shell>
@@ -80,6 +84,21 @@ const useDeveloperRoutes = (): NavItemProps[] => {
)
}
const useExtensionRoutes = (): NavItemProps[] => {
const links = routes.links
return useMemo(() => {
const settingsLinks = links.filter((link) =>
settingsRouteRegex.test(link.path)
)
return settingsLinks.map((link) => ({
label: link.label,
to: link.path,
}))
}, [links])
}
/**
* Ensure that the `from` prop is not another settings route, to avoid
* the user getting stuck in a navigation loop.
@@ -95,6 +114,8 @@ const getSafeFromValue = (from: string) => {
const SettingsSidebar = () => {
const routes = useSettingRoutes()
const developerRoutes = useDeveloperRoutes()
const extensionRoutes = useExtensionRoutes()
const { t } = useTranslation()
const location = useLocation()
@@ -108,20 +129,24 @@ const SettingsSidebar = () => {
return (
<aside className="flex flex-1 flex-col justify-between overflow-y-auto">
<div className="px-3 py-2">
<div className="flex items-center gap-x-3 p-1">
<Link to={from} replace className="flex items-center justify-center">
<IconButton size="small" variant="transparent">
<div className="p-3">
<div className="flex items-center gap-x-3 px-2 py-1.5">
<IconButton size="2xsmall" variant="transparent" asChild>
<Link
to={from}
replace
className="flex items-center justify-center"
>
<ArrowUturnLeft />
</IconButton>
</Link>
</Link>
</IconButton>
<Text leading="compact" weight="plus" size="small">
{t("nav.settings")}
</Text>
</div>
</div>
<div className="px-3">
<div className="border-ui-border-strong h-px w-full border-b border-dashed" />
<div className="flex items-center justify-center px-3">
<Divider variant="dashed" />
</div>
<div className="flex flex-1 flex-col overflow-y-auto">
<Collapsible.Root defaultOpen className="py-3">
@@ -147,6 +172,9 @@ const SettingsSidebar = () => {
</div>
</Collapsible.Content>
</Collapsible.Root>
<div className="flex items-center justify-center px-3">
<Divider variant="dashed" />
</div>
<Collapsible.Root defaultOpen className="py-3">
<div className="px-3">
<div className="text-ui-fg-muted flex h-7 items-center justify-between px-2">
@@ -170,6 +198,36 @@ const SettingsSidebar = () => {
</div>
</Collapsible.Content>
</Collapsible.Root>
{extensionRoutes.length > 0 && (
<Fragment>
<div className="flex items-center justify-center px-3">
<Divider variant="dashed" />
</div>
<Collapsible.Root defaultOpen className="py-3">
<div className="px-3">
<div className="text-ui-fg-muted flex h-7 items-center justify-between px-2">
<Text size="small" leading="compact">
{t("nav.extensions")}
</Text>
<Collapsible.Trigger asChild>
<IconButton size="2xsmall" variant="transparent">
<MinusMini className="text-ui-fg-muted" />
</IconButton>
</Collapsible.Trigger>
</div>
</div>
<Collapsible.Content>
<div className="pt-0.5">
<nav className="flex flex-col gap-y-1">
{extensionRoutes.map((setting) => (
<NavItem key={setting.to} {...setting} />
))}
</nav>
</div>
</Collapsible.Content>
</Collapsible.Root>
</Fragment>
)}
</div>
</aside>
)