All skills
medusajs avatar

/building-admin-dashboard-customizations

@42da91a official
by Medusamedusajs/medusa-agent-skills225 stars
29

Load automatically when planning, researching, or implementing Medusa Admin dashboard UI (widgets, custom pages, forms, tables, data loading, navigation). REQUIRED for all admin UI work in ALL modes (planning, implementation, exploration). Contains design patterns, component usage, and data loading patterns that MCP servers don't provide.

Use this Skill: https://skilld.dev/gh/medusajs/medusa-agent-skills/building-admin-dashboard-customizations

This session only. Nothing lands on disk.

referencesdisplay-patterns.md

≈2.8k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Displaying Entities - Patterns and Components

Contents

When to Use Each Pattern

Use DataTable when:

  • Displaying potentially many entries (>5-10 items)
  • Users need to search, filter, or paginate
  • Bulk actions are needed (select multiple, delete, etc.)
  • Displaying in a main list view

Use simple list components when:

  • Displaying a few entries (<5-10 items)
  • In a widget or sidebar context
  • As a preview or summary
  • Space is limited

DataTable Pattern

⚠️ pnpm Users: DataTable examples may use react-router-dom for navigation. Install it BEFORE implementing if needed.

Complete DataTable Implementation

import {
  DataTable,
  DataTableRowSelectionState,
  DataTablePaginationState,
  createDataTableColumnHelper,
  useDataTable,
} from "@medusajs/ui"
import { useState, useMemo } from "react"
import { useQuery } from "@tanstack/react-query"
import { sdk } from "../lib/client"

const columnHelper = createDataTableColumnHelper<HttpTypes.AdminProduct>()

const columns = [
  columnHelper.select(), // For row selection
  columnHelper.accessor("title", {
    header: "Title",
  }),
  columnHelper.accessor("status", {
    header: "Status",
  }),
  columnHelper.accessor("created_at", {
    header: "Created",
    cell: ({ getValue }) => new Date(getValue()).toLocaleDateString(),
  }),
]

export function ProductTable() {
  const [rowSelection, setRowSelection] = useState<DataTableRowSelectionState>(
    {}
  )
  const [searchValue, setSearchValue] = useState("")
  const [pagination, setPagination] = useState<DataTablePaginationState>({
    pageIndex: 0,
    pageSize: 15,
  })

  const limit = pagination.pageSize
  const offset = pagination.pageIndex * limit

  // Fetch products with search and pagination
  const { data, isLoading } = useQuery({
    queryFn: () =>
      sdk.admin.product.list({
        limit,
        offset,
        q: searchValue || undefined, // Search query
      }),
    queryKey: ["products", limit, offset, searchValue],
    keepPreviousData: true, // Smooth pagination
  })

  const table = useDataTable({
    data: data?.products || [],
    columns,
    getRowId: (product) => product.id,
    rowCount: data?.count || 0,
    isLoading,
    rowSelection: {
      state: rowSelection,
      onRowSelectionChange: setRowSelection,
    },
    search: {
      state: searchValue,
      onSearchChange: setSearchValue,
    },
    pagination: {
      state: pagination,
      onPaginationChange: setPagination,
    },
  })

  return (
    <DataTable instance={table}>
      <DataTable.Toolbar>
        <div className="flex gap-2">
          <DataTable.Search placeholder="Search products..." />
        </div>
      </DataTable.Toolbar>
      <DataTable.Table />
      <DataTable.Pagination />
    </DataTable>
  )
}

DataTable Troubleshooting

"DataTable.Search was rendered but search is not enabled"

You must pass search state configuration to useDataTable:

search: {
  state: searchValue,
  onSearchChange: setSearchValue,
}

"Cannot destructure property 'pageIndex' of pagination as it is undefined"

Always initialize pagination state with both properties:

const [pagination, setPagination] = useState({
  pageIndex: 0,
  pageSize: 15,
})

Simple List Patterns

Product/Variant List Item

For displaying a small list of products or variants with thumbnails:

import { Thumbnail, Text } from "@medusajs/ui"
import { TriangleRightMini } from "@medusajs/icons"
import { Link } from "react-router-dom"

// Component for displaying a product variant
const ProductVariantItem = ({ variant, link }) => {
  const Inner = (
    <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">
        <div className="shadow-elevation-card-rest rounded-md">
          <Thumbnail src={variant.product?.thumbnail} />
        </div>
        <div className="flex flex-1 flex-col">
          <Text size="small" leading="compact" weight="plus">
            {variant.title}
          </Text>
          <Text size="small" leading="compact" className="text-ui-fg-subtle">
            {variant.options.map((o) => o.value).join(" ⋅ ")}
          </Text>
        </div>
        <div className="size-7 flex items-center justify-center">
          <TriangleRightMini className="text-ui-fg-muted rtl:rotate-180" />
        </div>
      </div>
    </div>
  )

  if (!link) {
    return <div key={variant.id}>{Inner}</div>
  }

  return (
    <Link
      to={link}
      key={variant.id}
      className="outline-none focus-within:shadow-borders-interactive-with-focus rounded-md [&:hover>div]:bg-ui-bg-component-hover"
    >
      {Inner}
    </Link>
  )
}

// Usage in a widget
const RelatedProductsDisplay = ({ products }) => {
  if (products.length > 10) {
    // Use DataTable for many items
    return <ProductDataTable products={products} />
  }

  // Use simple list for few items
  return (
    <div className="flex flex-col gap-2">
      {products.map((product) => (
        <ProductVariantItem
          key={product.id}
          variant={product}
          link={`/products/${product.id}`}
        />
      ))}
    </div>
  )
}

Simple Text List (No Thumbnails)

For entities without images (categories, regions, etc.):

import { Text } from "@medusajs/ui"
import { TriangleRightMini } from "@medusajs/icons"
import { Link } from "react-router-dom"

const SimpleListItem = ({ title, description, link }) => {
  const Inner = (
    <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">
        <div className="flex flex-1 flex-col gap-y-1">
          <Text size="small" leading="compact" weight="plus">
            {title}
          </Text>
          {description && (
            <Text size="small" leading="compact" className="text-ui-fg-subtle">
              {description}
            </Text>
          )}
        </div>
        <div className="size-7 flex items-center justify-center">
          <TriangleRightMini className="text-ui-fg-muted rtl:rotate-180" />
        </div>
      </div>
    </div>
  )

  if (!link) {
    return <div>{Inner}</div>
  }

  return (
    <Link
      to={link}
      className="outline-none focus-within:shadow-borders-interactive-with-focus rounded-md [&:hover>div]:bg-ui-bg-component-hover"
    >
      {Inner}
    </Link>
  )
}

// Usage
<div className="flex flex-col gap-2">
  {categories.map((cat) => (
    <SimpleListItem
      key={cat.id}
      title={cat.name}
      description={cat.description}
      link={`/categories/${cat.id}`}
    />
  ))}
</div>

Compact List (No Cards)

For very compact displays:

import { Text } from "@medusajs/ui"

<div className="flex flex-col gap-y-2">
  {items.map((item) => (
    <div key={item.id} className="flex items-center justify-between">
      <Text size="small" leading="compact" weight="plus">
        {item.title}
      </Text>
      <Text size="small" leading="compact" className="text-ui-fg-subtle">
        {item.metadata}
      </Text>
    </div>
  ))}
</div>

Grid Display

For displaying items in a grid:

<div className="grid grid-cols-2 gap-4">
  {items.map((item) => (
    <div
      key={item.id}
      className="shadow-elevation-card-rest bg-ui-bg-component rounded-md p-4"
    >
      <div className="flex flex-col gap-y-2">
        <Thumbnail src={item.thumbnail} />
        <Text size="small" leading="compact" weight="plus">
          {item.title}
        </Text>
        <Text size="small" leading="compact" className="text-ui-fg-subtle">
          {item.description}
        </Text>
      </div>
    </div>
  ))}
</div>

Key Design Elements

For Product/Variant displays:

  • Always show the thumbnail using <Thumbnail /> component
  • Display title with <Text size="small" leading="compact" weight="plus">
  • Show secondary info with <Text size="small" leading="compact" className="text-ui-fg-subtle">
  • Use shadow-elevation-card-rest for card elevation
  • Include hover states with bg-ui-bg-component-hover
  • Add navigation indicators (arrows) when items are clickable

For other entities:

  • Use similar card patterns but adapt the content
  • Keep consistent spacing (gap-3 for items, gap-2 for lists)
  • Always use the Text component with correct typography patterns
  • Maintain visual hierarchy with weight="plus" for primary and text-ui-fg-subtle for secondary text

Empty States

Always handle empty states gracefully:

{items.length === 0 ? (
  <Text size="small" leading="compact" className="text-ui-fg-subtle">
    No items to display
  </Text>
) : (
  <div className="flex flex-col gap-2">
    {items.map((item) => (
      <ItemDisplay key={item.id} item={item} />
    ))}
  </div>
)}

Loading States

Show loading states while data is being fetched:

import { Spinner } from "@medusajs/ui"

{isLoading ? (
  <div className="flex items-center justify-center p-8">
    <Spinner />
  </div>
) : (
  <div className="flex flex-col gap-2">
    {items.map((item) => (
      <ItemDisplay key={item.id} item={item} />
    ))}
  </div>
)}

Conditional Rendering Based on Count

const DisplayComponent = ({ items }) => {
  // Use DataTable for many items
  if (items.length > 10) {
    return <ItemsDataTable items={items} />
  }

  // Use simple list for few items
  if (items.length > 0) {
    return (
      <div className="flex flex-col gap-2">
        {items.map((item) => (
          <SimpleListItem key={item.id} item={item} />
        ))}
      </div>
    )
  }

  // Empty state
  return (
    <Text size="small" leading="compact" className="text-ui-fg-subtle">
      No items to display
    </Text>
  )
}

Common Class Patterns

Card with elevation and hover

className="shadow-elevation-card-rest bg-ui-bg-component rounded-md transition-colors hover:bg-ui-bg-component-hover"

Flex container with consistent spacing

className="flex flex-col gap-2" // For vertical lists
className="flex items-center gap-3" // For horizontal items

Focus states for interactive elements

className="outline-none focus-within:shadow-borders-interactive-with-focus rounded-md"

RTL support for directional icons

className="text-ui-fg-muted rtl:rotate-180"

Source: SKILL.md on GitHub

1 warning16d4 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill provides comprehensive guidance, design patterns, and code examples for building Medusa Admin customizations. It promotes development best practices, such as using the official SDK for authenticated requests and Tanstack Query for efficient data loading. No security issues or malicious patterns were detected.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer7mo

    5/7 files flagged

Signed by skilld at 42da91a. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 days ago.

Activeupdated 2 months ago

README badge

README badge for medusajs/medusa-agent-skills/building-admin-dashboard-customizations