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

11 KiB

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"