feat(dashboard,admin-vite-plugin): Add support for outlet routes, loader, and handle (#11305)
**What**
- Add support for defining outlet routes using `@`, e.g. `/src/admin/routes/brands/@create/page.tsx`
- Add support for exporting a `loader` from a route file.
- Add support for exporting a `handle` from a route file.
Example usage of a loader and handle:
```tsx
// src/admin/routes/articles/[id]/page.tsx
import { Button, Container, Heading } from "@medusajs/ui";
import {
Link,
LoaderFunctionArgs,
Outlet,
UIMatch,
useLoaderData,
} from "react-router-dom";
export async function loader({ params }: LoaderFunctionArgs) {
const { id } = params;
return {
id,
};
}
export const handle = {
breadcrumb: (match: UIMatch<{ id: string }>) => {
const { id } = match.params;
return `#${id}`;
},
};
const ProfilePage = () => {
const { id } = useLoaderData() as Awaited<ReturnType<typeof loader>>;
return (
<div>
<Container className="flex justify-between items-center">
<Heading>Article {id}</Heading>
<Button size="small" variant="secondary" asChild>
<Link to="edit">Edit</Link>
</Button>
</Container>
{/* This will be used for the next example of an Outlet route */}
<Outlet />
</div>
);
};
export default ProfilePage;
```
In the above example we are passing data to the route from a loader, and defining a breadcrumb using the handle.
Example of a outlet route:
```tsx
// src/admin/routes/articles/[id]/@edit/page.tsx
import { Button, Container, Heading } from "@medusajs/ui";
const ProfileEditPage = () => {
return (
<div>
{/* Form goes here */}
</div>
);
};
export default ProfileEditPage;
```
This outlet route will be rendered in the <Outlet /> in the above example when the URL is /articles/1/edit
Resolves CMRC-913, CMRC-914, CMRC-915
This commit is contained in:
committed by
GitHub
parent
c08e6ad5cf
commit
a88f6576bd
@@ -54,15 +54,112 @@ const expectedRoutesWithoutLoaders = `
|
||||
routes: [
|
||||
{
|
||||
Component: RouteComponent0,
|
||||
path: "/one",
|
||||
path: "/one"
|
||||
},
|
||||
{
|
||||
Component: RouteComponent1,
|
||||
path: "/two",
|
||||
path: "/two"
|
||||
}
|
||||
]
|
||||
`
|
||||
|
||||
const mockFileContentsWithParallel = [
|
||||
// Parent route
|
||||
`
|
||||
import { defineRouteConfig } from "@medusajs/admin-sdk"
|
||||
const Page = () => {
|
||||
return <div>Brands</div>
|
||||
}
|
||||
export const config = defineRouteConfig({
|
||||
label: "Brands",
|
||||
})
|
||||
export default Page
|
||||
`,
|
||||
// Parallel route
|
||||
`
|
||||
import { defineRouteConfig } from "@medusajs/admin-sdk"
|
||||
const Page = () => {
|
||||
return <div>Create Brand</div>
|
||||
}
|
||||
export const config = defineRouteConfig({
|
||||
label: "Create Brand",
|
||||
})
|
||||
export default Page
|
||||
`,
|
||||
]
|
||||
|
||||
const expectedRoutesWithParallel = `
|
||||
routes: [
|
||||
{
|
||||
Component: RouteComponent0,
|
||||
path: "/brands",
|
||||
children: [
|
||||
{
|
||||
Component: RouteComponent1,
|
||||
path: "/brands/create"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
`
|
||||
|
||||
const mockFileContentsWithHandleLoader = [
|
||||
`
|
||||
import { defineRouteConfig } from "@medusajs/admin-sdk"
|
||||
|
||||
const Page = () => {
|
||||
return <div>Page 1</div>
|
||||
}
|
||||
|
||||
export const handle = {
|
||||
someConfig: true
|
||||
}
|
||||
|
||||
export const loader = async () => {
|
||||
return { data: true }
|
||||
}
|
||||
|
||||
export const config = defineRouteConfig({
|
||||
label: "Page 1",
|
||||
})
|
||||
|
||||
export default Page
|
||||
`,
|
||||
`
|
||||
import { defineRouteConfig } from "@medusajs/admin-sdk"
|
||||
|
||||
const Page = () => {
|
||||
return <div>Page 2</div>
|
||||
}
|
||||
|
||||
export async function loader() {
|
||||
return { data: true }
|
||||
}
|
||||
|
||||
export const config = defineRouteConfig({
|
||||
label: "Page 2",
|
||||
})
|
||||
|
||||
export default Page
|
||||
`,
|
||||
]
|
||||
|
||||
const expectedRoutesWithHandleLoader = `
|
||||
routes: [
|
||||
{
|
||||
Component: RouteComponent0,
|
||||
path: "/one",
|
||||
handle: handle0,
|
||||
loader: loader0
|
||||
},
|
||||
{
|
||||
Component: RouteComponent1,
|
||||
path: "/two",
|
||||
loader: loader1
|
||||
}
|
||||
]
|
||||
`
|
||||
|
||||
describe("generateRoutes", () => {
|
||||
it("should generate routes", async () => {
|
||||
const mockFiles = [
|
||||
@@ -103,4 +200,70 @@ describe("generateRoutes", () => {
|
||||
utils.normalizeString(expectedRoutesWithoutLoaders)
|
||||
)
|
||||
})
|
||||
it("should handle parallel routes", async () => {
|
||||
const mockFiles = [
|
||||
"Users/user/medusa/src/admin/routes/brands/page.tsx",
|
||||
"Users/user/medusa/src/admin/routes/brands/@create/page.tsx",
|
||||
]
|
||||
vi.mocked(utils.crawl).mockResolvedValue(mockFiles)
|
||||
|
||||
vi.mocked(fs.readFile).mockImplementation(async (file) =>
|
||||
Promise.resolve(
|
||||
mockFileContentsWithParallel[mockFiles.indexOf(file as string)]
|
||||
)
|
||||
)
|
||||
|
||||
vi.mocked(fs.stat).mockRejectedValue(new Error("File not found"))
|
||||
|
||||
const result = await generateRoutes(
|
||||
new Set(["Users/user/medusa/src/admin"])
|
||||
)
|
||||
expect(utils.normalizeString(result.code)).toEqual(
|
||||
utils.normalizeString(expectedRoutesWithParallel)
|
||||
)
|
||||
})
|
||||
it("should handle parallel routes with windows paths", async () => {
|
||||
const mockFiles = [
|
||||
"C:\\medusa\\src\\admin\\routes\\brands\\page.tsx",
|
||||
"C:\\medusa\\src\\admin\\routes\\brands\\@create\\page.tsx",
|
||||
]
|
||||
vi.mocked(utils.crawl).mockResolvedValue(mockFiles)
|
||||
|
||||
vi.mocked(fs.readFile).mockImplementation(async (file) =>
|
||||
Promise.resolve(
|
||||
mockFileContentsWithParallel[mockFiles.indexOf(file as string)]
|
||||
)
|
||||
)
|
||||
|
||||
vi.mocked(fs.stat).mockRejectedValue(new Error("File not found"))
|
||||
|
||||
const result = await generateRoutes(new Set(["C:\\medusa\\src\\admin"]))
|
||||
|
||||
expect(utils.normalizeString(result.code)).toEqual(
|
||||
utils.normalizeString(expectedRoutesWithParallel)
|
||||
)
|
||||
})
|
||||
it("should handle routes with handle and loader exports", async () => {
|
||||
const mockFiles = [
|
||||
"Users/user/medusa/src/admin/routes/one/page.tsx",
|
||||
"Users/user/medusa/src/admin/routes/two/page.tsx",
|
||||
]
|
||||
vi.mocked(utils.crawl).mockResolvedValue(mockFiles)
|
||||
|
||||
vi.mocked(fs.readFile).mockImplementation(async (file) =>
|
||||
Promise.resolve(
|
||||
mockFileContentsWithHandleLoader[mockFiles.indexOf(file as string)]
|
||||
)
|
||||
)
|
||||
|
||||
vi.mocked(fs.stat).mockRejectedValue(new Error("File not found"))
|
||||
|
||||
const result = await generateRoutes(
|
||||
new Set(["Users/user/medusa/src/admin"])
|
||||
)
|
||||
|
||||
expect(utils.normalizeString(result.code)).toEqual(
|
||||
utils.normalizeString(expectedRoutesWithHandleLoader)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user