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
@@ -1,5 +1,5 @@
|
||||
import { ComponentType } from "react"
|
||||
import { RouteObject } from "react-router-dom"
|
||||
import { LoaderFunction, RouteObject } from "react-router-dom"
|
||||
import { ErrorBoundary } from "../../components/utilities/error-boundary"
|
||||
import { RouteExtension, RouteModule } from "../types"
|
||||
|
||||
@@ -21,50 +21,176 @@ export const getRouteExtensions = (
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a route object for a branch node in the route tree
|
||||
* @param segment - The path segment for this branch
|
||||
*/
|
||||
const createBranchRoute = (segment: string): RouteObject => ({
|
||||
path: segment,
|
||||
children: [],
|
||||
})
|
||||
|
||||
/**
|
||||
* Creates a route object for a leaf node with its component
|
||||
* @param Component - The React component to render at this route
|
||||
*/
|
||||
const createLeafRoute = (
|
||||
Component: ComponentType,
|
||||
loader?: LoaderFunction,
|
||||
handle?: object
|
||||
): RouteObject => ({
|
||||
path: "",
|
||||
ErrorBoundary: ErrorBoundary,
|
||||
async lazy() {
|
||||
const result: {
|
||||
Component: ComponentType
|
||||
loader?: LoaderFunction
|
||||
handle?: object
|
||||
} = { Component }
|
||||
|
||||
if (loader) {
|
||||
result.loader = loader
|
||||
}
|
||||
|
||||
if (handle) {
|
||||
result.handle = handle
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Creates a parallel route configuration
|
||||
* @param path - The route path
|
||||
* @param Component - The React component to render
|
||||
*/
|
||||
const createParallelRoute = (
|
||||
path: string,
|
||||
Component: ComponentType,
|
||||
loader?: LoaderFunction,
|
||||
handle?: object
|
||||
) => ({
|
||||
path,
|
||||
async lazy() {
|
||||
const result: {
|
||||
Component: ComponentType
|
||||
loader?: LoaderFunction
|
||||
handle?: object
|
||||
} = { Component }
|
||||
|
||||
if (loader) {
|
||||
result.loader = loader
|
||||
}
|
||||
|
||||
if (handle) {
|
||||
result.handle = handle
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Processes parallel routes by cleaning their paths relative to the current path
|
||||
* @param parallelRoutes - Array of parallel route extensions
|
||||
* @param currentFullPath - The full path of the current route
|
||||
*/
|
||||
const processParallelRoutes = (
|
||||
parallelRoutes: RouteExtension[] | undefined,
|
||||
currentFullPath: string
|
||||
): RouteObject[] | undefined => {
|
||||
return parallelRoutes
|
||||
?.map(({ path, Component, loader, handle }) => {
|
||||
const childPath = path?.replace(currentFullPath, "").replace(/^\/+/, "")
|
||||
if (!childPath) {
|
||||
return null
|
||||
}
|
||||
return createParallelRoute(childPath, Component, loader, handle)
|
||||
})
|
||||
.filter(Boolean) as RouteObject[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively builds the route tree by adding routes at the correct level
|
||||
* @param pathSegments - Array of remaining path segments to process
|
||||
* @param Component - The React component for the route
|
||||
* @param currentLevel - Current level in the route tree
|
||||
* @param parallelRoutes - Optional parallel routes to add
|
||||
* @param fullPath - The full path up to the current level
|
||||
*/
|
||||
const addRoute = (
|
||||
pathSegments: string[],
|
||||
Component: ComponentType,
|
||||
currentLevel: RouteObject[],
|
||||
loader?: LoaderFunction,
|
||||
handle?: object,
|
||||
parallelRoutes?: RouteExtension[],
|
||||
fullPath?: string
|
||||
) => {
|
||||
if (!pathSegments.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const [currentSegment, ...remainingSegments] = pathSegments
|
||||
let route = currentLevel.find((r) => r.path === currentSegment)
|
||||
|
||||
if (!route) {
|
||||
route = createBranchRoute(currentSegment)
|
||||
currentLevel.push(route)
|
||||
}
|
||||
|
||||
const currentFullPath = fullPath
|
||||
? `${fullPath}/${currentSegment}`
|
||||
: currentSegment
|
||||
|
||||
if (remainingSegments.length === 0) {
|
||||
route.children ||= []
|
||||
const leaf = createLeafRoute(Component, loader)
|
||||
|
||||
/**
|
||||
* The handle needs to be set on the wrapper route object,
|
||||
* in order for it to be resolved correctly thoughout
|
||||
* the branch.
|
||||
*/
|
||||
if (handle) {
|
||||
route.handle = handle
|
||||
}
|
||||
|
||||
leaf.children = processParallelRoutes(parallelRoutes, currentFullPath)
|
||||
route.children.push(leaf)
|
||||
} else {
|
||||
route.children ||= []
|
||||
addRoute(
|
||||
remainingSegments,
|
||||
Component,
|
||||
route.children,
|
||||
loader,
|
||||
handle,
|
||||
parallelRoutes,
|
||||
currentFullPath
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a complete route map from route extensions
|
||||
* @param routes - Array of route extensions to process
|
||||
* @param ignore - Optional path prefix to ignore when processing routes
|
||||
* @returns An array of route objects forming a route tree
|
||||
*/
|
||||
export const createRouteMap = (
|
||||
routes: RouteExtension[],
|
||||
ignore?: string
|
||||
): RouteObject[] => {
|
||||
const root: RouteObject[] = []
|
||||
|
||||
const addRoute = (
|
||||
pathSegments: string[],
|
||||
Component: ComponentType,
|
||||
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: "",
|
||||
ErrorBoundary: ErrorBoundary,
|
||||
async lazy() {
|
||||
return { Component }
|
||||
},
|
||||
})
|
||||
} else {
|
||||
route.children ||= []
|
||||
addRoute(remainingSegments, Component, route.children)
|
||||
}
|
||||
}
|
||||
|
||||
routes.forEach(({ path, Component }) => {
|
||||
routes.forEach(({ path, Component, loader, handle, children }) => {
|
||||
const cleanedPath = ignore
|
||||
? path.replace(ignore, "").replace(/^\/+/, "")
|
||||
: path.replace(/^\/+/, "")
|
||||
const pathSegments = cleanedPath.split("/").filter(Boolean)
|
||||
addRoute(pathSegments, Component, root)
|
||||
addRoute(pathSegments, Component, root, loader, handle, children)
|
||||
})
|
||||
|
||||
return root
|
||||
|
||||
@@ -13,6 +13,8 @@ import { ZodFirstPartySchemaTypes } from "zod"
|
||||
export type RouteExtension = {
|
||||
Component: ComponentType
|
||||
loader?: LoaderFunction
|
||||
handle?: object
|
||||
children?: RouteExtension[]
|
||||
path: string
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user