Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/mean-mugs-sparkle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@medusajs/admin-vite-plugin": patch
"@medusajs/dashboard": patch
---

feat(admin-vite-plugin,dashboard): support for react-router's splat and optional segments
6 changes: 5 additions & 1 deletion packages/admin/admin-vite-plugin/src/routes/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ export function getRoute(file: string): string {
const importPath = normalizePath(file)
return importPath
.replace(/.*\/admin\/(routes)/, "")
.replace(/\[([^\]]+)\]/g, ":$1")
.replace("[[*]]", "*?") // optional splat
.replace("[*]", "*") // splat
.replace(/\(([^\[\]\)]+)\)/g, "$1?") // optional static, (foo)
.replace(/\[\[([^\]]+)\]\]/g, ":$1?") // optional dynamic, [[foo]]
.replace(/\[([^\]]+)\]/g, ":$1") // dynamic, [foo]
.replace(
new RegExp(
`/page\\.(${VALID_FILE_EXTENSIONS.map((ext) => ext.slice(1)).join(
Expand Down
12 changes: 10 additions & 2 deletions packages/admin/dashboard/src/dashboard-app/dashboard-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ type DashboardAppProps = {
plugins: DashboardPlugin[]
}

/**
* Matches segments that are optional and at the end of the path.
* Example: /path/to/:id?
* Such paths can be added to the menu items without the optional segment.
*/
const OPTIONAL_LAST_SEGMENT_MATCH = /\/([^\/])+\?$/

export class DashboardApp {
private widgets: WidgetMap
private menus: MenuMap
Expand Down Expand Up @@ -124,10 +131,11 @@ export class DashboardApp {
allMenuItems.sort((a, b) => a.path.length - b.path.length)

allMenuItems.forEach((item) => {
if (item.path.includes("/:")) {
item.path = item.path.replace(OPTIONAL_LAST_SEGMENT_MATCH, "")
if (item.path.includes("/:") || item.path.endsWith("/*")) {
if (process.env.NODE_ENV === "development") {
console.warn(
`[@medusajs/dashboard] Menu item for path "${item.path}" can't be added to the sidebar as it contains a parameter.`
`[@medusajs/dashboard] Menu item for path "${item.path}" can't be added to the sidebar as it contains a mandatory parameter.`
)
}
return
Expand Down
20 changes: 14 additions & 6 deletions packages/admin/dashboard/src/dashboard-app/routes/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,10 @@ const createBranchRoute = (segment: string): RouteObject => ({
const createLeafRoute = (
Component: ComponentType,
loader?: LoaderFunction,
handle?: object
handle?: object,
path: string = ""
): RouteObject => ({
path: "",
path,
ErrorBoundary: ErrorBoundary,
async lazy() {
const result: {
Expand Down Expand Up @@ -149,7 +150,6 @@ const addRoute = (

if (isComponentSegment || remainingSegments.length === 0) {
route.children ||= []
const leaf = createLeafRoute(Component, loader)

if (handle) {
route.handle = handle
Expand All @@ -159,9 +159,17 @@ const addRoute = (
route.loader = loader
}

leaf.children = processParallelRoutes(parallelRoutes, currentFullPath)
route.children.push(leaf)

// Since splat segments must occur at the end of a route, react-router enforces the segment to be a leaf.
// Therefore we can't create a child leaf route with `path: ""` and must instead modify the route itself
if (currentSegment === "*?" || currentSegment === "*") {
const leaf = createLeafRoute(Component, loader, handle, currentSegment)
leaf.children = processParallelRoutes(parallelRoutes, currentFullPath)
Object.assign(route, leaf)
} else {
const leaf = createLeafRoute(Component, loader)
leaf.children = processParallelRoutes(parallelRoutes, currentFullPath)
route.children.push(leaf)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Splat Route Configuration Overwrite Bug

For splat routes (* or *?), Object.assign(route, leaf) incorrectly overwrites the route's path, handle, loader, and children. This causes the route to lose its parent path and configuration, and can lead to errors if children becomes undefined. The code also processes remaining segments after a splat route, which violates React Router's terminal splat segment rule, creating invalid route structures.

Fix in Cursor Fix in Web

if (remainingSegments.length > 0) {
addRoute(
remainingSegments,
Expand Down
Loading