feat(dashboard,admin-vite-plugin,admin-bundler,admin-sdk): Rework admin extensions and introduce custom fields API (#9338)
This commit is contained in:
@@ -19,14 +19,13 @@ 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 { INavItem, NavItem } 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 { useDashboardExtension } from "../../../extensions"
|
||||
import { useLogout } from "../../../hooks/api"
|
||||
import { queryClient } from "../../../lib/query-client"
|
||||
import { useSearch } from "../../../providers/search-provider"
|
||||
@@ -177,7 +176,7 @@ const Header = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const useCoreRoutes = (): Omit<NavItemProps, "pathname">[] => {
|
||||
const useCoreRoutes = (): Omit<INavItem, "pathname">[] => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return [
|
||||
@@ -297,14 +296,11 @@ const CoreRouteSection = () => {
|
||||
|
||||
const ExtensionRouteSection = () => {
|
||||
const { t } = useTranslation()
|
||||
const { getMenu } = useDashboardExtension()
|
||||
|
||||
const links = routes.links
|
||||
const menuItems = getMenu("coreExtensions")
|
||||
|
||||
const extensionLinks = links
|
||||
.filter((link) => !settingsRouteRegex.test(link.path))
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
|
||||
if (!extensionLinks.length) {
|
||||
if (!menuItems.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -330,13 +326,14 @@ const ExtensionRouteSection = () => {
|
||||
</div>
|
||||
<Collapsible.Content>
|
||||
<nav className="flex flex-col gap-y-0.5 py-1 pb-4">
|
||||
{extensionLinks.map((link) => {
|
||||
{menuItems.map((item, i) => {
|
||||
return (
|
||||
<NavItem
|
||||
key={link.path}
|
||||
to={link.path}
|
||||
label={link.label}
|
||||
icon={link.icon ? <link.icon /> : <SquaresPlus />}
|
||||
key={i}
|
||||
to={item.to}
|
||||
label={item.label}
|
||||
icon={item.icon ? item.icon : <SquaresPlus />}
|
||||
items={item.items}
|
||||
type="extension"
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ type NestedItemProps = {
|
||||
to: string
|
||||
}
|
||||
|
||||
export type NavItemProps = {
|
||||
export type INavItem = {
|
||||
icon?: ReactNode
|
||||
label: string
|
||||
to: string
|
||||
@@ -29,11 +29,11 @@ export type NavItemProps = {
|
||||
}
|
||||
|
||||
const BASE_NAV_LINK_CLASSES =
|
||||
"text-ui-fg-subtle transition-fg hover:bg-ui-bg-subtle-hover flex items-center gap-x-2 rounded-md py-1 pl-0.5 pr-2 outline-none [&>svg]:text-ui-fg-subtle focus-visible:shadow-borders-focus"
|
||||
"text-ui-fg-subtle transition-fg hover:bg-ui-bg-subtle-hover flex items-center gap-x-2 rounded-md py-0.5 pl-0.5 pr-2 outline-none [&>svg]:text-ui-fg-subtle focus-visible:shadow-borders-focus"
|
||||
const ACTIVE_NAV_LINK_CLASSES =
|
||||
"bg-ui-bg-base shadow-elevation-card-rest text-ui-fg-base hover:bg-ui-bg-base"
|
||||
const NESTED_NAV_LINK_CLASSES = "pl-[34px] pr-2 w-full text-ui-fg-muted"
|
||||
const SETTING_NAV_LINK_CLASSES = "pl-2"
|
||||
const NESTED_NAV_LINK_CLASSES = "pl-[34px] pr-2 py-1 w-full text-ui-fg-muted"
|
||||
const SETTING_NAV_LINK_CLASSES = "pl-2 py-1"
|
||||
|
||||
const getIsOpen = (
|
||||
to: string,
|
||||
@@ -89,7 +89,7 @@ export const NavItem = ({
|
||||
items,
|
||||
type = "core",
|
||||
from,
|
||||
}: NavItemProps) => {
|
||||
}: INavItem) => {
|
||||
const { pathname } = useLocation()
|
||||
const [open, setOpen] = useState(getIsOpen(to, items, pathname))
|
||||
|
||||
@@ -149,7 +149,7 @@ export const NavItem = ({
|
||||
<Collapsible.Root open={open} onOpenChange={setOpen}>
|
||||
<Collapsible.Trigger
|
||||
className={clx(
|
||||
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover flex w-full items-center gap-x-2 rounded-md py-1 pl-0.5 pr-2 outline-none lg:hidden",
|
||||
"text-ui-fg-subtle hover:text-ui-fg-base transition-fg hover:bg-ui-bg-subtle-hover flex w-full items-center gap-x-2 rounded-md py-0.5 pl-0.5 pr-2 outline-none lg:hidden",
|
||||
{ "pl-2": isSetting }
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -126,17 +126,17 @@ const Notification = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-start justify-center gap-3 border-b p-6 relative">
|
||||
<div className="relative flex items-start justify-center gap-3 border-b p-6">
|
||||
<div className="text-ui-fg-muted flex size-5 items-center justify-center">
|
||||
<InformationCircleSolid />
|
||||
</div>
|
||||
<div className="flex w-full flex-col gap-y-3">
|
||||
<div className="flex flex-col">
|
||||
<div className="items-center flex justify-between">
|
||||
<div className="flex items-center justify-between">
|
||||
<Text size="small" leading="compact" weight="plus">
|
||||
{data.title}
|
||||
</Text>
|
||||
<div className="items-center flex justify-center align-center gap-2">
|
||||
<div className="align-center flex items-center justify-center gap-2">
|
||||
<Text
|
||||
as={"span"}
|
||||
className={clx("text-ui-fg-subtle", {
|
||||
@@ -152,7 +152,7 @@ const Notification = ({
|
||||
</Text>
|
||||
{unread && (
|
||||
<div
|
||||
className="h-2 w-2 rounded bg-ui-bg-interactive"
|
||||
className="bg-ui-bg-interactive h-2 w-2 rounded"
|
||||
role="status"
|
||||
/>
|
||||
)}
|
||||
@@ -201,7 +201,7 @@ const useUnreadNotifications = () => {
|
||||
const [hasUnread, setHasUnread] = useState(false)
|
||||
const { notifications } = useNotifications(
|
||||
{ limit: 1, offset: 0, fields: "created_at" },
|
||||
{ refetchInterval: 3000 }
|
||||
{ refetchInterval: 60_000 }
|
||||
)
|
||||
const lastNotification = notifications?.[0]
|
||||
|
||||
|
||||
+4
-4
@@ -48,12 +48,12 @@ export const SingleColumnPage = <TData,>({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{before.widgets.map((w, i) => {
|
||||
return <w.Component {...widgetProps} key={i} />
|
||||
{before.map((Component, i) => {
|
||||
return <Component {...widgetProps} key={i} />
|
||||
})}
|
||||
{children}
|
||||
{after.widgets.map((w, i) => {
|
||||
return <w.Component {...widgetProps} key={i} />
|
||||
{after.map((Component, i) => {
|
||||
return <Component {...widgetProps} key={i} />
|
||||
})}
|
||||
{showMetadata && <MetadataSection data={data!} />}
|
||||
{showJSON && <JsonViewSection data={data!} />}
|
||||
|
||||
+12
-12
@@ -1,13 +1,13 @@
|
||||
import { clx } from "@medusajs/ui"
|
||||
import { Children, ComponentPropsWithoutRef } from "react"
|
||||
import { Children, ComponentPropsWithoutRef, ComponentType } from "react"
|
||||
import { Outlet } from "react-router-dom"
|
||||
import { JsonViewSection } from "../../../common/json-view-section"
|
||||
import { MetadataSection } from "../../../common/metadata-section"
|
||||
import { PageProps, WidgetImport, WidgetProps } from "../types"
|
||||
import { PageProps, WidgetProps } from "../types"
|
||||
|
||||
interface TwoColumnWidgetProps extends WidgetProps {
|
||||
sideBefore: WidgetImport
|
||||
sideAfter: WidgetImport
|
||||
sideBefore: ComponentType<any>[]
|
||||
sideAfter: ComponentType<any>[]
|
||||
}
|
||||
|
||||
interface TwoColumnPageProps<TData> extends PageProps<TData> {
|
||||
@@ -71,14 +71,14 @@ const Root = <TData,>({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{before.widgets.map((w, i) => {
|
||||
return <w.Component {...widgetProps} key={i} />
|
||||
{before.map((Component, i) => {
|
||||
return <Component {...widgetProps} key={i} />
|
||||
})}
|
||||
<div className="flex flex-col gap-x-4 gap-y-3 xl:flex-row xl:items-start">
|
||||
<div className="flex w-full flex-col gap-y-3">
|
||||
{main}
|
||||
{after.widgets.map((w, i) => {
|
||||
return <w.Component {...widgetProps} key={i} />
|
||||
{after.map((Component, i) => {
|
||||
return <Component {...widgetProps} key={i} />
|
||||
})}
|
||||
{showExtraData && (
|
||||
<div className="hidden flex-col gap-y-3 xl:flex">
|
||||
@@ -88,12 +88,12 @@ const Root = <TData,>({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex w-full max-w-[100%] flex-col gap-y-3 xl:mt-0 xl:max-w-[440px]">
|
||||
{sideBefore.widgets.map((w, i) => {
|
||||
return <w.Component {...widgetProps} key={i} />
|
||||
{sideBefore.map((Component, i) => {
|
||||
return <Component {...widgetProps} key={i} />
|
||||
})}
|
||||
{sidebar}
|
||||
{sideAfter.widgets.map((w, i) => {
|
||||
return <w.Component {...widgetProps} key={i} />
|
||||
{sideAfter.map((Component, i) => {
|
||||
return <Component {...widgetProps} key={i} />
|
||||
})}
|
||||
{showExtraData && (
|
||||
<div className="flex flex-col gap-y-3 xl:hidden">
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
import { ReactNode } from "react"
|
||||
|
||||
export type Widget = {
|
||||
Component: React.ComponentType<any>
|
||||
}
|
||||
|
||||
export type WidgetImport = {
|
||||
widgets: Widget[]
|
||||
}
|
||||
import { ComponentType, ReactNode } from "react"
|
||||
|
||||
export interface WidgetProps {
|
||||
before: WidgetImport
|
||||
after: WidgetImport
|
||||
before: ComponentType<any>[]
|
||||
after: ComponentType<any>[]
|
||||
}
|
||||
|
||||
export interface PageProps<TData> {
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { Outlet } from "react-router-dom"
|
||||
|
||||
export const PublicLayout = () => {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center px-4 py-6">
|
||||
<div className="bg-ui-bg-base text-ui-fg-subtle w-[520px] px-16 py-20 rounded-[32px] shadow-elevation-modal flex flex-col gap-y-12 items-center">
|
||||
<div className="w-24 h-24 rounded-3xl bg-ui-bg-subtle shadow-elevation-card-hover"></div>
|
||||
<div className="w-full">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return <Outlet />
|
||||
}
|
||||
|
||||
@@ -5,12 +5,11 @@ 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 { INavItem, NavItem } from "../nav-item"
|
||||
import { Shell } from "../shell"
|
||||
|
||||
import routes from "virtual:medusa/routes/links"
|
||||
import { useDashboardExtension } from "../../../extensions"
|
||||
import { UserMenu } from "../user-menu"
|
||||
|
||||
export const SettingsLayout = () => {
|
||||
@@ -21,7 +20,7 @@ export const SettingsLayout = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const useSettingRoutes = (): NavItemProps[] => {
|
||||
const useSettingRoutes = (): INavItem[] => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
@@ -67,7 +66,7 @@ const useSettingRoutes = (): NavItemProps[] => {
|
||||
)
|
||||
}
|
||||
|
||||
const useDeveloperRoutes = (): NavItemProps[] => {
|
||||
const useDeveloperRoutes = (): INavItem[] => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
@@ -89,7 +88,7 @@ const useDeveloperRoutes = (): NavItemProps[] => {
|
||||
)
|
||||
}
|
||||
|
||||
const useMyAccountRoutes = (): NavItemProps[] => {
|
||||
const useMyAccountRoutes = (): INavItem[] => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return useMemo(
|
||||
@@ -103,21 +102,6 @@ const useMyAccountRoutes = (): 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.
|
||||
@@ -131,10 +115,12 @@ const getSafeFromValue = (from: string) => {
|
||||
}
|
||||
|
||||
const SettingsSidebar = () => {
|
||||
const { getMenu } = useDashboardExtension()
|
||||
|
||||
const routes = useSettingRoutes()
|
||||
const developerRoutes = useDeveloperRoutes()
|
||||
const extensionRoutes = useExtensionRoutes()
|
||||
const myAccountRoutes = useMyAccountRoutes()
|
||||
const extensionRoutes = getMenu("settingsExtensions")
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -227,7 +213,7 @@ const CollapsibleSection = ({
|
||||
items,
|
||||
}: {
|
||||
label: string
|
||||
items: NavItemProps[]
|
||||
items: INavItem[]
|
||||
}) => {
|
||||
return (
|
||||
<Collapsible.Root defaultOpen className="py-3">
|
||||
|
||||
Reference in New Issue
Block a user