feat(dashboard): ability to locate new admin route under existing route (#10587)

This PR add ability to locate new admin route under existing route in sidebar.

For example, new route Brands
![image](https://github.com/user-attachments/assets/1b297fb0-296c-4e94-a4cb-f84f4c676c53)
![image](https://github.com/user-attachments/assets/80336909-1c0a-49c9-b8e1-3b1137ae2e48)

https://github.com/user-attachments/assets/b46b1813-e92e-4b67-84a1-84660023ac7c
This commit is contained in:
Eugene Pro
2024-12-19 14:23:21 +02:00
committed by GitHub
parent 16ae192456
commit 3efd25d06d
11 changed files with 131 additions and 26 deletions

View File

@@ -44,6 +44,21 @@ const mockFileContents = [
label: "Page 2",
})
export default Page
`,
`
import { defineRouteConfig } from "@medusajs/admin-sdk"
const Page = () => {
return <div>Page 2</div>
}
export const config = defineRouteConfig({
label: "Page 3",
icon: "icon1",
nested: "/products"
})
export default Page
`,
]
@@ -54,11 +69,19 @@ const expectedMenuItems = `
label: RouteConfig0.label,
icon: RouteConfig0.icon,
path: "/one",
nested: undefined
},
{
label: RouteConfig1.label,
icon: undefined,
path: "/two",
nested: undefined
},
{
label: RouteConfig2.label,
icon: RouteConfig2.icon,
path: "/three",
nested: "/products"
}
]
`
@@ -68,6 +91,7 @@ describe("generateMenuItems", () => {
const mockFiles = [
"Users/user/medusa/src/admin/routes/one/page.tsx",
"Users/user/medusa/src/admin/routes/two/page.tsx",
"Users/user/medusa/src/admin/routes/three/page.tsx",
]
vi.mocked(utils.crawl).mockResolvedValue(mockFiles)
@@ -82,6 +106,7 @@ describe("generateMenuItems", () => {
expect(result.imports).toEqual([
`import { config as RouteConfig0 } from "Users/user/medusa/src/admin/routes/one/page.tsx"`,
`import { config as RouteConfig1 } from "Users/user/medusa/src/admin/routes/two/page.tsx"`,
`import { config as RouteConfig2 } from "Users/user/medusa/src/admin/routes/three/page.tsx"`,
])
expect(utils.normalizeString(result.code)).toEqual(
utils.normalizeString(expectedMenuItems)
@@ -93,6 +118,7 @@ describe("generateMenuItems", () => {
const mockFiles = [
"C:\\medusa\\src\\admin\\routes\\one\\page.tsx",
"C:\\medusa\\src\\admin\\routes\\two\\page.tsx",
"C:\\medusa\\src\\admin\\routes\\three\\page.tsx",
]
vi.mocked(utils.crawl).mockResolvedValue(mockFiles)
@@ -105,6 +131,7 @@ describe("generateMenuItems", () => {
expect(result.imports).toEqual([
`import { config as RouteConfig0 } from "C:/medusa/src/admin/routes/one/page.tsx"`,
`import { config as RouteConfig1 } from "C:/medusa/src/admin/routes/two/page.tsx"`,
`import { config as RouteConfig2 } from "C:/medusa/src/admin/routes/three/page.tsx"`,
])
expect(utils.normalizeString(result.code)).toEqual(
utils.normalizeString(expectedMenuItems)

View File

@@ -16,11 +16,19 @@ import {
normalizePath,
} from "../utils"
import { getRoute } from "./helpers"
import { NESTED_ROUTE_POSITIONS } from "@medusajs/admin-shared"
type RouteConfig = {
label: boolean
icon: boolean
nested?: string
}
type MenuItem = {
icon?: string
label: string
path: string
nested?: string
}
type MenuItemResult = {
@@ -32,13 +40,10 @@ export async function generateMenuItems(sources: Set<string>) {
const files = await getFilesFromSources(sources)
const results = await getMenuItemResults(files)
const imports = results.map((result) => result.import).flat()
const imports = results.map((result) => result.import)
const code = generateCode(results)
return {
imports,
code,
}
return { imports, code }
}
function generateCode(results: MenuItemResult[]): string {
@@ -53,10 +58,12 @@ function generateCode(results: MenuItemResult[]): string {
}
function formatMenuItem(route: MenuItem): string {
const { label, icon, path, nested } = route
return `{
label: ${route.label},
icon: ${route.icon ? route.icon : "undefined"},
path: "${route.path}",
label: ${label},
icon: ${icon || "undefined"},
path: "${path}",
nested: ${nested ? `"${nested}"` : "undefined"}
}`
}
@@ -107,25 +114,21 @@ function generateImport(file: string, index: number): string {
}
function generateMenuItem(
config: { label: boolean; icon: boolean },
config: RouteConfig,
file: string,
index: number
): MenuItem {
const configName = generateRouteConfigName(index)
const routePath = getRoute(file)
return {
label: `${configName}.label`,
icon: config.icon ? `${configName}.icon` : undefined,
path: routePath,
path: getRoute(file),
nested: config.nested,
}
}
async function getRouteConfig(
file: string
): Promise<{ label: boolean; icon: boolean } | null> {
async function getRouteConfig(file: string): Promise<RouteConfig | null> {
const code = await fs.readFile(file, "utf-8")
let ast: ParseResult<File> | null = null
try {
@@ -138,32 +141,50 @@ async function getRouteConfig(
return null
}
let config: { label: boolean; icon: boolean } | null = null
let config: RouteConfig | null = null
try {
traverse(ast, {
ExportNamedDeclaration(path) {
const properties = getConfigObjectProperties(path)
if (!properties) {
return
}
const hasLabel = properties.some(
(prop) =>
isObjectProperty(prop) && isIdentifier(prop.key, { name: "label" })
)
const hasProperty = (name: string) =>
properties.some(
(prop) => isObjectProperty(prop) && isIdentifier(prop.key, { name })
)
const hasLabel = hasProperty("label")
if (!hasLabel) {
return
}
const hasIcon = properties.some(
const nested = properties.find(
(prop) =>
isObjectProperty(prop) && isIdentifier(prop.key, { name: "icon" })
isObjectProperty(prop) && isIdentifier(prop.key, { name: "nested" })
)
config = { label: hasLabel, icon: hasIcon }
const nestedValue = nested ? (nested as any).value.value : undefined
if (nestedValue && !NESTED_ROUTE_POSITIONS.includes(nestedValue)) {
logger.error(
`Invalid nested route position: "${nestedValue}". Allowed values are: ${NESTED_ROUTE_POSITIONS.join(
", "
)}`,
{
file,
}
)
return
}
config = {
label: hasLabel,
icon: hasProperty("icon"),
nested: nestedValue,
}
},
})
} catch (e) {