497 lines
13 KiB
Markdown
497 lines
13 KiB
Markdown
# Navigation and Routing
|
|
|
|
## Contents
|
|
- [Pre-Implementation Requirements for pnpm](#pre-implementation-requirements-for-pnpm)
|
|
- [Basic Navigation with Link Component](#basic-navigation-with-link-component)
|
|
- [Programmatic Navigation](#programmatic-navigation)
|
|
- [Accessing Route Parameters](#accessing-route-parameters)
|
|
- [Linking to Built-in Admin Pages](#linking-to-built-in-admin-pages)
|
|
- [Navigation from Widgets](#navigation-from-widgets)
|
|
- [Common Navigation Patterns](#common-navigation-patterns)
|
|
|
|
## Pre-Implementation Requirements for pnpm
|
|
|
|
**⚠️ pnpm Users**: Navigation requires `react-router-dom`. Install BEFORE implementing:
|
|
|
|
```bash
|
|
pnpm list react-router-dom --depth=10 | grep @medusajs/dashboard
|
|
pnpm add react-router-dom@[exact-version]
|
|
```
|
|
|
|
**npm/yarn Users**: DO NOT install - already available through dashboard dependencies.
|
|
|
|
## Basic Navigation with Link Component
|
|
|
|
Use the `Link` component for internal navigation in widgets and custom pages:
|
|
|
|
```tsx
|
|
import { Link } from "react-router-dom"
|
|
import { Text } from "@medusajs/ui"
|
|
import { TriangleRightMini } from "@medusajs/icons"
|
|
|
|
// Link to a custom page
|
|
<Link
|
|
to="/custom/my-page"
|
|
className="outline-none focus-within:shadow-borders-interactive-with-focus rounded-md [&:hover>div]:bg-ui-bg-component-hover"
|
|
>
|
|
<div className="shadow-elevation-card-rest bg-ui-bg-component rounded-md px-4 py-3 transition-colors">
|
|
<div className="flex items-center gap-3">
|
|
<Text size="small" leading="compact" weight="plus">
|
|
Go to Custom Page
|
|
</Text>
|
|
<div className="size-7 flex items-center justify-center">
|
|
<TriangleRightMini className="text-ui-fg-muted rtl:rotate-180" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
```
|
|
|
|
### Link with Dynamic ID
|
|
|
|
```tsx
|
|
// Link to product details
|
|
<Link to={`/products/${product.id}`}>
|
|
<Text size="small" leading="compact" weight="plus">
|
|
{product.title}
|
|
</Text>
|
|
</Link>
|
|
```
|
|
|
|
### Button-styled Link
|
|
|
|
```tsx
|
|
import { Button } from "@medusajs/ui"
|
|
import { Link } from "react-router-dom"
|
|
|
|
<Button asChild size="small" variant="secondary">
|
|
<Link to="/custom/my-page">
|
|
View Details
|
|
</Link>
|
|
</Button>
|
|
```
|
|
|
|
## Programmatic Navigation
|
|
|
|
Use `useNavigate` for navigation after actions (e.g., after creating an entity):
|
|
|
|
```tsx
|
|
import { useNavigate } from "react-router-dom"
|
|
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
|
import { toast, Button } from "@medusajs/ui"
|
|
import { sdk } from "../lib/client"
|
|
|
|
const CreateProductWidget = () => {
|
|
const navigate = useNavigate()
|
|
const queryClient = useQueryClient()
|
|
|
|
const createProduct = useMutation({
|
|
mutationFn: (data) => sdk.admin.product.create(data),
|
|
onSuccess: (result) => {
|
|
queryClient.invalidateQueries({ queryKey: ["products"] })
|
|
toast.success("Product created successfully")
|
|
|
|
// Navigate to the new product's page
|
|
navigate(`/products/${result.product.id}`)
|
|
},
|
|
onError: (error) => {
|
|
toast.error(error.message || "Failed to create product")
|
|
},
|
|
})
|
|
|
|
const handleCreate = () => {
|
|
createProduct.mutate({
|
|
title: "New Product",
|
|
// ... other fields
|
|
})
|
|
}
|
|
|
|
return (
|
|
<Button onClick={handleCreate} isLoading={createProduct.isPending}>
|
|
Create and View Product
|
|
</Button>
|
|
)
|
|
}
|
|
```
|
|
|
|
### Navigate with State
|
|
|
|
Pass data to the destination page:
|
|
|
|
```tsx
|
|
navigate("/custom/review", {
|
|
state: { productId: product.id, productTitle: product.title }
|
|
})
|
|
|
|
// Access in destination page
|
|
import { useLocation } from "react-router-dom"
|
|
|
|
const ReviewPage = () => {
|
|
const location = useLocation()
|
|
const { productId, productTitle } = location.state || {}
|
|
|
|
return <div>Reviewing: {productTitle}</div>
|
|
}
|
|
```
|
|
|
|
### Navigate Back
|
|
|
|
```tsx
|
|
const navigate = useNavigate()
|
|
|
|
<Button onClick={() => navigate(-1)}>
|
|
Go Back
|
|
</Button>
|
|
```
|
|
|
|
## Accessing Route Parameters
|
|
|
|
In custom pages, access URL parameters with `useParams`:
|
|
|
|
```tsx
|
|
// Custom page at: /custom/products/:id
|
|
import { useParams } from "react-router-dom"
|
|
import { useQuery } from "@tanstack/react-query"
|
|
import { sdk } from "../lib/client"
|
|
import { Container, Heading } from "@medusajs/ui"
|
|
|
|
const ProductDetailsPage = () => {
|
|
const { id } = useParams() // Get :id from URL
|
|
|
|
const { data: product, isLoading } = useQuery({
|
|
queryFn: () => sdk.admin.product.retrieve(id, {
|
|
fields: "+metadata,+variants.*",
|
|
}),
|
|
queryKey: ["product", id],
|
|
enabled: !!id, // Only fetch if ID exists
|
|
})
|
|
|
|
if (isLoading) return <div>Loading...</div>
|
|
|
|
return (
|
|
<Container>
|
|
<Heading>{product?.title}</Heading>
|
|
{/* Product details */}
|
|
</Container>
|
|
)
|
|
}
|
|
|
|
export default ProductDetailsPage
|
|
```
|
|
|
|
### Multiple Parameters
|
|
|
|
```tsx
|
|
// Route: /custom/orders/:orderId/items/:itemId
|
|
const { orderId, itemId } = useParams()
|
|
|
|
const { data } = useQuery({
|
|
queryFn: () => sdk.client.fetch(`/admin/orders/${orderId}/items/${itemId}`),
|
|
queryKey: ["order-item", orderId, itemId],
|
|
enabled: !!orderId && !!itemId,
|
|
})
|
|
```
|
|
|
|
### Query Parameters
|
|
|
|
Use `useSearchParams` for query string parameters:
|
|
|
|
```tsx
|
|
import { useSearchParams } from "react-router-dom"
|
|
|
|
const ProductsPage = () => {
|
|
const [searchParams, setSearchParams] = useSearchParams()
|
|
|
|
const status = searchParams.get("status") // Get ?status=published
|
|
const page = searchParams.get("page") // Get ?page=2
|
|
|
|
const { data } = useQuery({
|
|
queryFn: () => sdk.admin.product.list({
|
|
status,
|
|
offset: (parseInt(page) || 0) * 15,
|
|
}),
|
|
queryKey: ["products", status, page],
|
|
})
|
|
|
|
// Update query params
|
|
const handleFilterChange = (newStatus: string) => {
|
|
setSearchParams({ status: newStatus, page: "0" })
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<Button onClick={() => handleFilterChange("published")}>
|
|
Published Only
|
|
</Button>
|
|
{/* Products list */}
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
## Linking to Built-in Admin Pages
|
|
|
|
Link to standard Medusa admin pages:
|
|
|
|
```tsx
|
|
import { Link } from "react-router-dom"
|
|
|
|
// Product details
|
|
<Link to={`/products/${productId}`}>View Product</Link>
|
|
|
|
// Order details
|
|
<Link to={`/orders/${orderId}`}>View Order</Link>
|
|
|
|
// Customer details
|
|
<Link to={`/customers/${customerId}`}>View Customer</Link>
|
|
|
|
// Product categories
|
|
<Link to="/categories">View Categories</Link>
|
|
|
|
// Settings
|
|
<Link to="/settings">Settings</Link>
|
|
|
|
// Custom field in settings
|
|
<Link to="/settings/custom-field-name">Custom Settings</Link>
|
|
```
|
|
|
|
### Common Built-in Routes
|
|
|
|
```tsx
|
|
const ADMIN_ROUTES = {
|
|
products: "/products",
|
|
productDetails: (id: string) => `/products/${id}`,
|
|
orders: "/orders",
|
|
orderDetails: (id: string) => `/orders/${id}`,
|
|
customers: "/customers",
|
|
customerDetails: (id: string) => `/customers/${id}`,
|
|
categories: "/categories",
|
|
inventory: "/inventory",
|
|
pricing: "/pricing",
|
|
settings: "/settings",
|
|
}
|
|
|
|
// Usage
|
|
<Link to={ADMIN_ROUTES.productDetails(product.id)}>
|
|
View Product
|
|
</Link>
|
|
```
|
|
|
|
## Navigation from Widgets
|
|
|
|
### Pattern: View All Link
|
|
|
|
Add a "View All" link from a widget to a custom page:
|
|
|
|
```tsx
|
|
import { defineWidgetConfig } from "@medusajs/admin-sdk"
|
|
import { Container, Heading, Button, Text } from "@medusajs/ui"
|
|
import { Link } from "react-router-dom"
|
|
import { DetailWidgetProps } from "@medusajs/framework/types"
|
|
|
|
const RelatedProductsWidget = ({ data: product }) => {
|
|
// ... widget logic
|
|
|
|
return (
|
|
<Container className="divide-y p-0">
|
|
<div className="flex items-center justify-between px-6 py-4">
|
|
<Heading level="h2">Related Products</Heading>
|
|
<div className="flex items-center gap-x-2">
|
|
<Button size="small" variant="secondary" onClick={() => setOpen(true)}>
|
|
Edit
|
|
</Button>
|
|
<Button asChild size="small" variant="transparent">
|
|
<Link to={`/custom/products/${product.id}/related`}>
|
|
View All
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
{/* Widget content */}
|
|
</Container>
|
|
)
|
|
}
|
|
|
|
export const config = defineWidgetConfig({
|
|
zone: "product.details.after",
|
|
})
|
|
|
|
export default RelatedProductsWidget
|
|
```
|
|
|
|
### Pattern: List Item Navigation
|
|
|
|
Make list items clickable to navigate:
|
|
|
|
```tsx
|
|
import { Thumbnail, Text } from "@medusajs/ui"
|
|
import { TriangleRightMini } from "@medusajs/icons"
|
|
import { Link } from "react-router-dom"
|
|
|
|
const ProductListItem = ({ product }) => {
|
|
return (
|
|
<Link
|
|
to={`/products/${product.id}`}
|
|
className="outline-none focus-within:shadow-borders-interactive-with-focus rounded-md [&:hover>div]:bg-ui-bg-component-hover"
|
|
>
|
|
<div className="shadow-elevation-card-rest bg-ui-bg-component rounded-md px-4 py-2 transition-colors">
|
|
<div className="flex items-center gap-3">
|
|
<Thumbnail src={product.thumbnail} />
|
|
<div className="flex flex-1 flex-col">
|
|
<Text size="small" leading="compact" weight="plus">
|
|
{product.title}
|
|
</Text>
|
|
<Text size="small" leading="compact" className="text-ui-fg-subtle">
|
|
{product.status}
|
|
</Text>
|
|
</div>
|
|
<div className="size-7 flex items-center justify-center">
|
|
<TriangleRightMini className="text-ui-fg-muted rtl:rotate-180" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
)
|
|
}
|
|
```
|
|
|
|
## Common Navigation Patterns
|
|
|
|
### Pattern: Back to List
|
|
|
|
Navigate back to list after viewing details:
|
|
|
|
```tsx
|
|
import { useNavigate } from "react-router-dom"
|
|
import { ArrowLeft } from "@medusajs/icons"
|
|
import { IconButton } from "@medusajs/ui"
|
|
|
|
const DetailsPage = () => {
|
|
const navigate = useNavigate()
|
|
|
|
return (
|
|
<div>
|
|
<div className="flex items-center gap-x-2 mb-4">
|
|
<IconButton onClick={() => navigate("/custom/products")}>
|
|
<ArrowLeft />
|
|
</IconButton>
|
|
<Heading>Product Details</Heading>
|
|
</div>
|
|
{/* Details content */}
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
### Pattern: Breadcrumb Navigation
|
|
|
|
```tsx
|
|
import { Link } from "react-router-dom"
|
|
import { Text } from "@medusajs/ui"
|
|
|
|
const Breadcrumbs = ({ product }) => {
|
|
return (
|
|
<div className="flex items-center gap-x-2">
|
|
<Link to="/products">
|
|
<Text size="small" className="text-ui-fg-subtle hover:text-ui-fg-base">
|
|
Products
|
|
</Text>
|
|
</Link>
|
|
<Text size="small" className="text-ui-fg-muted">/</Text>
|
|
<Link to={`/products/${product.id}`}>
|
|
<Text size="small" className="text-ui-fg-subtle hover:text-ui-fg-base">
|
|
{product.title}
|
|
</Text>
|
|
</Link>
|
|
<Text size="small" className="text-ui-fg-muted">/</Text>
|
|
<Text size="small" weight="plus">Details</Text>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
### Pattern: Tab Navigation
|
|
|
|
Navigate between different views using tabs:
|
|
|
|
```tsx
|
|
import { useSearchParams, Link } from "react-router-dom"
|
|
import { Tabs } from "@medusajs/ui"
|
|
|
|
const ProductTabs = () => {
|
|
const [searchParams] = useSearchParams()
|
|
const activeTab = searchParams.get("tab") || "details"
|
|
|
|
return (
|
|
<Tabs value={activeTab}>
|
|
<Tabs.List>
|
|
<Tabs.Trigger value="details" asChild>
|
|
<Link to="?tab=details">Details</Link>
|
|
</Tabs.Trigger>
|
|
<Tabs.Trigger value="variants" asChild>
|
|
<Link to="?tab=variants">Variants</Link>
|
|
</Tabs.Trigger>
|
|
<Tabs.Trigger value="media" asChild>
|
|
<Link to="?tab=media">Media</Link>
|
|
</Tabs.Trigger>
|
|
</Tabs.List>
|
|
|
|
<Tabs.Content value="details">
|
|
{/* Details content */}
|
|
</Tabs.Content>
|
|
<Tabs.Content value="variants">
|
|
{/* Variants content */}
|
|
</Tabs.Content>
|
|
<Tabs.Content value="media">
|
|
{/* Media content */}
|
|
</Tabs.Content>
|
|
</Tabs>
|
|
)
|
|
}
|
|
```
|
|
|
|
### Pattern: Action with Navigation
|
|
|
|
Perform an action then navigate:
|
|
|
|
```tsx
|
|
const deleteProduct = useMutation({
|
|
mutationFn: (id) => sdk.admin.product.delete(id),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: ["products"] })
|
|
toast.success("Product deleted")
|
|
navigate("/products") // Navigate to list after deletion
|
|
},
|
|
})
|
|
```
|
|
|
|
### Pattern: Conditional Navigation
|
|
|
|
Navigate based on state or data:
|
|
|
|
```tsx
|
|
const handleComplete = () => {
|
|
if (hasErrors) {
|
|
toast.error("Please fix errors first")
|
|
return
|
|
}
|
|
|
|
if (isDraft) {
|
|
navigate(`/custom/products/${id}/publish`)
|
|
} else {
|
|
navigate("/products")
|
|
}
|
|
}
|
|
```
|
|
|
|
## Important Notes
|
|
|
|
1. **pnpm users**: Must install `react-router-dom` with exact version from dashboard
|
|
2. **npm/yarn users**: Do NOT install `react-router-dom` - already available
|
|
3. **Always use relative paths** starting with `/` for internal navigation
|
|
4. **Use Link for navigation links** - better for SEO and accessibility
|
|
5. **Use navigate for programmatic navigation** - after actions or based on logic
|
|
6. **Always handle loading states** when fetching route parameter-based data
|
|
7. **Clean up on unmount** when using listeners or subscriptions in routes
|
|
8. **Maintain focus management** for accessibility when navigating
|