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:
committed by
GitHub
parent
521c252dee
commit
f1176a0673
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+67
-9
@@ -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>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { RouteObject } from "react-router-dom"
|
||||
|
||||
/**
|
||||
* Used to test if a route is a settings route.
|
||||
*/
|
||||
export const settingsRouteRegex = /^\/settings\//
|
||||
|
||||
export const createRouteMap = (
|
||||
routes: { path: string; file: string }[],
|
||||
ignore?: string
|
||||
): RouteObject[] => {
|
||||
const root: RouteObject[] = []
|
||||
|
||||
const addRoute = (
|
||||
pathSegments: string[],
|
||||
file: string,
|
||||
currentLevel: RouteObject[]
|
||||
) => {
|
||||
if (!pathSegments.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const [currentSegment, ...remainingSegments] = pathSegments
|
||||
let route = currentLevel.find((r) => r.path === currentSegment)
|
||||
|
||||
if (!route) {
|
||||
route = { path: currentSegment, children: [] }
|
||||
currentLevel.push(route)
|
||||
}
|
||||
|
||||
if (remainingSegments.length === 0) {
|
||||
route.children ||= []
|
||||
route.children.push({
|
||||
path: "",
|
||||
async lazy() {
|
||||
const { default: Component } = await import(/* @vite-ignore */ file)
|
||||
return { Component }
|
||||
},
|
||||
})
|
||||
} else {
|
||||
route.children ||= []
|
||||
addRoute(remainingSegments, file, route.children)
|
||||
}
|
||||
}
|
||||
|
||||
routes.forEach(({ path, file }) => {
|
||||
// Remove the ignore segment from the path if it is provided
|
||||
const cleanedPath = ignore
|
||||
? path.replace(ignore, "").replace(/^\/+/, "")
|
||||
: path.replace(/^\/+/, "")
|
||||
const pathSegments = cleanedPath.split("/").filter(Boolean)
|
||||
addRoute(pathSegments, file, root)
|
||||
})
|
||||
|
||||
return root
|
||||
}
|
||||
+13
-27
@@ -1,4 +1,4 @@
|
||||
declare module "medusa-admin:widgets/*" {
|
||||
declare module "virtual:medusa/widgets/*" {
|
||||
const widgets: { Component: () => JSX.Element }[]
|
||||
|
||||
export default {
|
||||
@@ -6,34 +6,20 @@ declare module "medusa-admin:widgets/*" {
|
||||
}
|
||||
}
|
||||
|
||||
declare module "medusa-admin:routes/links" {
|
||||
const links: { path: string; label: string; icon?: React.ComponentType }[]
|
||||
declare module "virtual:medusa/routes/pages" {
|
||||
const pages: { path: string; file: string }[]
|
||||
|
||||
export default {
|
||||
pages,
|
||||
}
|
||||
}
|
||||
|
||||
declare module "virtual:medusa/routes/links" {
|
||||
import type { ComponentType } from "react"
|
||||
|
||||
const links: { path: string; label: string; icon?: ComponentType }[]
|
||||
|
||||
export default {
|
||||
links,
|
||||
}
|
||||
}
|
||||
|
||||
declare module "medusa-admin:routes/pages" {
|
||||
const pages: { path: string; file: string }[]
|
||||
|
||||
export default {
|
||||
pages,
|
||||
}
|
||||
}
|
||||
|
||||
declare module "medusa-admin:settings/cards" {
|
||||
const cards: { path: string; label: string; description: string }[]
|
||||
|
||||
export default {
|
||||
cards,
|
||||
}
|
||||
}
|
||||
|
||||
declare module "medusa-admin:settings/pages" {
|
||||
const pages: { path: string; file: string }[]
|
||||
|
||||
export default {
|
||||
pages,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import { RouteObject } from "react-router-dom"
|
||||
import routes from "virtual:medusa/routes/pages"
|
||||
|
||||
// import routes from "medusa-admin:routes/pages"
|
||||
import { createRouteMap, settingsRouteRegex } from "../../lib/extension-helpers"
|
||||
|
||||
export const RouteExtensions: RouteObject[] = []
|
||||
const pages = routes.pages
|
||||
.filter((ext) => !settingsRouteRegex.test(ext.path))
|
||||
.map((ext) => ext)
|
||||
|
||||
/**
|
||||
* UI Route extensions.
|
||||
* Core Route extensions.
|
||||
*/
|
||||
// export const RouteExtensions: RouteObject[] = routes.pages.map((ext) => {
|
||||
// return {
|
||||
// path: ext.path,
|
||||
// async lazy() {
|
||||
// const { default: Component } = await import(/* @vite-ignore */ ext.file)
|
||||
// return { Component }
|
||||
// },
|
||||
// }
|
||||
// })
|
||||
export const RouteExtensions = createRouteMap(pages)
|
||||
|
||||
@@ -517,9 +517,9 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
...RouteExtensions,
|
||||
],
|
||||
},
|
||||
...RouteExtensions,
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -1009,9 +1009,9 @@ export const RouteMap: RouteObject[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
...SettingsExtensions,
|
||||
],
|
||||
},
|
||||
...SettingsExtensions,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
+8
-15
@@ -1,19 +1,12 @@
|
||||
import { RouteObject } from "react-router-dom"
|
||||
import routes from "virtual:medusa/routes/pages"
|
||||
|
||||
// import settings from "medusa-admin:settings/pages"
|
||||
import { createRouteMap, settingsRouteRegex } from "../../lib/extension-helpers"
|
||||
|
||||
const pages = routes.pages
|
||||
.filter((ext) => settingsRouteRegex.test(ext.path))
|
||||
.map((ext) => ext)
|
||||
|
||||
/**
|
||||
* UI Settings extensions.
|
||||
* Settings Route extensions.
|
||||
*/
|
||||
|
||||
export const SettingsExtensions: RouteObject[] = []
|
||||
|
||||
// export const SettingsExtensions: RouteObject[] = settings.pages.map((ext) => {
|
||||
// return {
|
||||
// path: `/settings${ext.path}`,
|
||||
// async lazy() {
|
||||
// const { default: Component } = await import(/* @vite-ignore */ ext.file)
|
||||
// return { Component }
|
||||
// },
|
||||
// }
|
||||
// })
|
||||
export const SettingsExtensions = createRouteMap(pages, "/settings")
|
||||
|
||||
@@ -10,7 +10,9 @@ import { ProductVariantSection } from "./components/product-variant-section"
|
||||
import { productLoader } from "./loader"
|
||||
|
||||
// import after from "medusa-admin:widgets/product/details/after"
|
||||
// import before from "medusa-admin:widgets/product/details/before"
|
||||
// @ts-ignore - virtual module
|
||||
// import obj from "virtual:config"
|
||||
import before from "virtual:medusa/widgets/product/details/before"
|
||||
// import sideAfter from "medusa-admin:widgets/product/details/side/after"
|
||||
// import sideBefore from "medusa-admin:widgets/product/details/side/before"
|
||||
|
||||
@@ -38,14 +40,13 @@ export const ProductDetail = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{/* {before.widgets.map((w, i) => {
|
||||
{before.widgets.map((w, i) => {
|
||||
return (
|
||||
<div key={i}>
|
||||
<w.Component />
|
||||
</div>
|
||||
)
|
||||
})} */}
|
||||
|
||||
})}
|
||||
<div className="flex flex-col gap-x-4 lg:flex-row lg:items-start">
|
||||
<div className="flex w-full flex-col gap-y-2">
|
||||
<ProductGeneralSection product={product} />
|
||||
|
||||
Reference in New Issue
Block a user