Skip to content

Commit

Permalink
feat(dashboard,admin-vite-plugin): Add support for outlet routes, loa…
Browse files Browse the repository at this point in the history
…der, 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
  • Loading branch information
kasperkristensen authored Feb 13, 2025
1 parent c08e6ad commit a88f657
Show file tree
Hide file tree
Showing 6 changed files with 485 additions and 58 deletions.
6 changes: 6 additions & 0 deletions .changeset/rude-balloons-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@medusajs/admin-vite-plugin": patch
"@medusajs/dashboard": patch
---

feat(dashboard,admin-vite-plugin): Add support for parallel routes
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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)
)
})
})
Loading

0 comments on commit a88f657

Please sign in to comment.