Files
suplement/.agents/skills/building-admin-dashboard-customizations/references/navigation.md
2026-03-07 11:07:45 -03:00

13 KiB

Navigation and Routing

Contents

Pre-Implementation Requirements for pnpm

⚠️ pnpm Users: Navigation requires react-router-dom. Install BEFORE implementing:

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.

Use the Link component for internal navigation in widgets and custom pages:

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 to product details
<Link to={`/products/${product.id}`}>
  <Text size="small" leading="compact" weight="plus">
    {product.title}
  </Text>
</Link>
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):

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:

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

const navigate = useNavigate()

<Button onClick={() => navigate(-1)}>
  Go Back
</Button>

Accessing Route Parameters

In custom pages, access URL parameters with useParams:

// 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

// 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:

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:

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

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

Add a "View All" link from a widget to a custom page:

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:

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:

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

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:

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:

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:

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