building-admin-dashboard-customizations

作者: medusajs

使用管理員SDK和Medusa UI元件為Medusa管理後台自訂UI擴充功能。進行任何管理員UI工作(規劃、實作、探索)時,請優先載入此技能;MCP伺服器僅提供API參考,不包含設計模式或資料載入策略。關鍵:所有API請求務必使用Medusa JS SDK(絕不使用一般fetch);將顯示查詢與模態查詢分離,並在變更後使顯示資料失效。在現有頁面上實作小工具或建立自訂UI路由;...

npx skills add https://github.com/medusajs/medusa-agent-skills --skill building-admin-dashboard-customizations

Medusa Admin Dashboard Customizations

Build custom UI extensions for the Medusa Admin dashboard using the Admin SDK and Medusa UI components.

Note: "UI Routes" are custom admin pages, different from backend API routes (which use building-with-medusa skill).

When to Apply

Load this skill for ANY admin UI development task, including:

  • Creating widgets for product/order/customer pages
  • Building custom admin pages
  • Implementing forms and modals
  • Displaying data with tables or lists
  • Adding navigation between pages

Also load these skills when:

  • building-with-medusa: Building backend API routes that the admin UI calls
  • building-storefronts: If working on storefront instead of admin dashboard

CRITICAL: Load Reference Files When Needed

The quick reference below is NOT sufficient for implementation. You MUST load relevant reference files before writing code for that component.

Load these references based on what you're implementing:

  • Creating widgets? → MUST load references/data-loading.md first
  • Building forms/modals? → MUST load references/forms.md first
  • Displaying data in tables/lists? → MUST load references/display-patterns.md first
  • Selecting from large datasets? → MUST load references/table-selection.md first
  • Adding navigation? → MUST load references/navigation.md first
  • Styling components? → MUST load references/typography.md first

Minimum requirement: Load at least 1-2 reference files relevant to your specific task before implementing.

When to Use This Skill vs MedusaDocs MCP Server

⚠️ CRITICAL: This skill should be consulted FIRST for planning and implementation.

Use this skill for (PRIMARY SOURCE):

  • Planning - Understanding how to structure admin UI features
  • Component patterns - Widgets, pages, forms, tables, modals
  • Design system - Typography, colors, spacing, semantic classes
  • Data loading - Critical separate query pattern, cache invalidation
  • Best practices - Correct vs incorrect patterns (e.g., display queries on mount)
  • Critical rules - What NOT to do (common mistakes like conditional display queries)

Use MedusaDocs MCP server for (SECONDARY SOURCE):

  • Specific component prop signatures after you know which component to use
  • Available widget zones list
  • JS SDK method details
  • Configuration options reference

Why skills come first:

  • Skills contain critical patterns like separate display/modal queries that MCP doesn't emphasize
  • Skills show correct vs incorrect patterns; MCP shows what's possible
  • Planning requires understanding patterns, not just API reference

Critical Setup Rules

SDK Client Configuration

CRITICAL: Always use exact configuration - different values cause errors:

// src/admin/lib/client.ts
import Medusa from "@medusajs/js-sdk"

export const sdk = new Medusa({
  baseUrl: import.meta.env.VITE_BACKEND_URL || "/",
  debug: import.meta.env.DEV,
  auth: {
    type: "session",
  },
})

pnpm Users ONLY

CRITICAL: Install peer dependencies BEFORE writing any code:

# Find exact version from dashboard
pnpm list @tanstack/react-query --depth=10 | grep @medusajs/dashboard
# Install that exact version
pnpm add @tanstack/react-query@[exact-version]

# If using navigation (Link component)
pnpm list react-router-dom --depth=10 | grep @medusajs/dashboard
pnpm add react-router-dom@[exact-version]

npm/yarn users: DO NOT install these packages - already available.

Rule Categories by Priority

PriorityCategoryImpactPrefix
1Data LoadingCRITICALdata-
2Design SystemCRITICALdesign-
3Data DisplayHIGH (includes CRITICAL price rule)display-
4TypographyHIGHtypo-
5Forms & ModalsMEDIUMform-
6Selection PatternsMEDIUMselect-

Quick Reference

1. Data Loading (CRITICAL)

  • data-sdk-always - ALWAYS use Medusa JS SDK for ALL API requests - NEVER use regular fetch() (missing auth headers causes errors)
  • data-sdk-method-choice - Use existing SDK methods for built-in endpoints (sdk.admin.product.list()), use sdk.client.fetch() for custom routes
  • data-display-on-mount - Display queries MUST load on mount (no enabled condition based on UI state)
  • data-separate-queries - Separate display queries from modal/form queries
  • data-invalidate-display - Invalidate display queries after mutations, not just modal queries
  • data-loading-states - Always show loading states (Spinner), not empty states
  • data-pnpm-install-first - pnpm users MUST install @tanstack/react-query BEFORE coding

2. Design System (CRITICAL)

  • design-semantic-colors - Always use semantic color classes (bg-ui-bg-base, text-ui-fg-subtle), never hardcoded
  • design-spacing - Use px-6 py-4 for section padding, gap-2 for lists, gap-3 for items
  • design-button-size - Always use size="small" for buttons in widgets and tables
  • design-medusa-components - Always use Medusa UI components (Container, Button, Text), not raw HTML

3. Data Display (HIGH)

  • display-price-format - CRITICAL: Prices from Medusa are stored as-is ($49.99 = 49.99, NOT in cents). Display them directly - NEVER divide by 100

4. Typography (HIGH)

  • typo-text-component - Always use Text component from @medusajs/ui, never plain span/p tags
  • typo-labels - Use <Text size="small" leading="compact" weight="plus"> for labels/headings
  • typo-descriptions - Use <Text size="small" leading="compact" className="text-ui-fg-subtle"> for descriptions
  • typo-no-heading-widgets - Never use Heading for small sections in widgets (use Text instead)

5. Forms & Modals (MEDIUM)

  • form-focusmodal-create - Use FocusModal for creating new entities
  • form-drawer-edit - Use Drawer for editing existing entities
  • form-disable-pending - Always disable actions during mutations (disabled={mutation.isPending})
  • form-show-loading - Show loading state on submit button (isLoading={mutation.isPending})

6. Selection Patterns (MEDIUM)

  • select-small-datasets - Use Select component for 2-10 options (statuses, types, etc.)
  • select-large-datasets - Use DataTable with FocusModal for large datasets (products, categories, etc.)
  • select-search-config - Must pass search configuration to useDataTable to avoid "search not enabled" error

Critical Data Loading Pattern

ALWAYS follow this pattern - never load display data conditionally:

// ✅ CORRECT - Separate queries with proper responsibilities
const RelatedProductsWidget = ({ data: product }) => {
  const [modalOpen, setModalOpen] = useState(false)

  // Display query - loads on mount
  const { data: displayProducts } = useQuery({
    queryFn: () => fetchSelectedProducts(selectedIds),
    queryKey: ["related-products-display", product.id],
    // No 'enabled' condition - loads immediately
  })

  // Modal query - loads when needed
  const { data: modalProducts } = useQuery({
    queryFn: () => sdk.admin.product.list({ limit: 10, offset: 0 }),
    queryKey: ["products-selection"],
    enabled: modalOpen, // OK for modal-only data
  })

  // Mutation with proper invalidation
  const updateProduct = useMutation({
    mutationFn: updateFunction,
    onSuccess: () => {
      // Invalidate display data query to refresh UI
      queryClient.invalidateQueries({ queryKey: ["related-products-display", product.id] })
      // Also invalidate the entity query
      queryClient.invalidateQueries({ queryKey: ["product", product.id] })
      // Note: No need to invalidate modal selection query
    },
  })

  return (
    <Container>
      {/* Display uses displayProducts */}
      {displayProducts?.map(p => <div key={p.id}>{p.title}</div>)}

      <FocusModal open={modalOpen} onOpenChange={setModalOpen}>
        {/* Modal uses modalProducts */}
      </FocusModal>
    </Container>
  )
}

// ❌ WRONG - Single query with conditional loading
const BrokenWidget = ({ data: product }) => {
  const [modalOpen, setModalOpen] = useState(false)

  const { data } = useQuery({
    queryFn: () => sdk.admin.product.list(),
    enabled: modalOpen, // ❌ Display breaks on page refresh!
  })

  // Trying to display from modal query
  const displayItems = data?.filter(item => ids.includes(item.id)) // No data until modal opens

  return <div>{displayItems?.map(...)}</div> // Empty on mount!
}

Why this matters:

  • On page refresh, modal is closed, so conditional query doesn't run
  • User sees empty state instead of their data
  • Display depends on modal interaction (broken UX)

Common Mistakes Checklist

Before implementing, verify you're NOT doing these:

Data Loading:

  • Using regular fetch() instead of Medusa JS SDK (causes missing auth header errors)
  • Not using existing SDK methods for built-in endpoints (e.g., using sdk.client.fetch("/admin/products") instead of sdk.admin.product.list())
  • Loading display data conditionally based on modal/UI state
  • Using a single query for both display and modal
  • Forgetting to invalidate display queries after mutations
  • Not handling loading states (showing empty instead of spinner)
  • pnpm users: Not installing @tanstack/react-query before coding

Design System:

  • Using hardcoded colors instead of semantic classes
  • Forgetting size="small" on buttons in widgets
  • Not using px-6 py-4 for section padding
  • Using raw HTML elements instead of Medusa UI components

Data Display:

  • CRITICAL: Dividing prices by 100 when displaying (prices are stored as-is: $49.99 = 49.99, NOT in cents)

Typography:

  • Using plain span/p tags instead of Text component
  • Not using weight="plus" for labels
  • Not using text-ui-fg-subtle for descriptions
  • Using Heading in small widget sections

Forms:

  • Using Drawer for creating (should use FocusModal)
  • Using FocusModal for editing (should use Drawer)
  • Not disabling buttons during mutations
  • Not showing loading state on submit

Selection:

  • Using DataTable for <10 items (overkill)
  • Using Select for >10 items (poor UX)
  • Not configuring search in useDataTable (causes error)

Reference Files Available

Load these for detailed patterns:

references/data-loading.md       - useQuery/useMutation patterns, cache invalidation
references/forms.md              - FocusModal/Drawer patterns, validation
references/table-selection.md    - Complete DataTable selection pattern
references/display-patterns.md   - Lists, tables, cards for entities
references/typography.md         - Text component patterns
references/navigation.md         - Link, useNavigate, useParams patterns

Each reference contains:

  • Step-by-step implementation guides
  • Correct vs incorrect code examples
  • Common mistakes and solutions
  • Complete working examples

Integration with Backend

⚠️ CRITICAL: ALWAYS use the Medusa JS SDK for ALL API requests - NEVER use regular fetch()

Admin UI connects to backend API routes using the SDK:

import { sdk } from "[LOCATE SDK INSTANCE IN PROJECT]"

// ✅ CORRECT - Built-in endpoint: Use existing SDK method
const { data: product } = useQuery({
  queryKey: ["product", productId],
  queryFn: () => sdk.admin.product.retrieve(productId),
})

// ✅ CORRECT - Custom endpoint: Use sdk.client.fetch()
const { data: reviews } = useQuery({
  queryKey: ["reviews", product.id],
  queryFn: () => sdk.client.fetch(`/admin/products/${product.id}/reviews`),
})

// ❌ WRONG - Using regular fetch
const { data } = useQuery({
  queryKey: ["reviews", product.id],
  queryFn: () => fetch(`http://localhost:9000/admin/products/${product.id}/reviews`),
  // ❌ Error: Missing Authorization header!
})

// Mutation to custom backend route
const createReview = useMutation({
  mutationFn: (data) => sdk.client.fetch("/admin/reviews", {
    method: "POST",
    body: data
  }),
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ["reviews", product.id] })
    toast.success("Review created")
  },
})

Why the SDK is required:

  • Admin routes need Authorization and session cookie headers
  • Store routes need x-publishable-api-key header
  • SDK handles all required headers automatically
  • Regular fetch() without headers → authentication/authorization errors
  • Using existing SDK methods provides better type safety

When to use what:

  • Built-in endpoints: Use existing SDK methods (sdk.admin.product.list(), sdk.store.product.list())
  • Custom endpoints: Use sdk.client.fetch() for your custom API routes

For implementing backend API routes, load the building-with-medusa skill.

Widget vs UI Route

Widgets extend existing admin pages:

// src/admin/widgets/custom-widget.tsx
import { defineWidgetConfig } from "@medusajs/admin-sdk"
import { DetailWidgetProps } from "@medusajs/framework/types"

const MyWidget = ({ data }: DetailWidgetProps<HttpTypes.AdminProduct>) => {
  return <Container>Widget content</Container>
}

export const config = defineWidgetConfig({
  zone: "product.details",
})

export default MyWidget

⚠️ .before / .after no longer control placement (v2.17.2+):

Since the Layout Composer landed, admin users arrange components — including widgets — through the dashboard's Editor view, and the arrangement is persisted in the database. The .before and .after zone suffixes are deprecated: a widget in product.details.before and one in product.details.after land in the same injection zone, and the final order is whatever the user configured.

  • Don't promise the user a specific position based on the suffix. Say "the widget appears in the product details page and can be repositioned in the Editor view".
  • .side is still meaningful — it targets the side column of two-column page layouts.
  • For new widgets, prefer the unsuffixed zone (e.g. product.details) unless the project already standardizes on a suffix.

Newer zones added in v2.16.0 cover draft orders, gift cards, and store credit accounts (draft_order.*, gift_card.*, store_credit_account.*, in details/list/side variants). Ask the MedusaDocs MCP server for the authoritative zone list rather than guessing a zone name.

UI Routes create new admin pages:

// src/admin/routes/custom-page/page.tsx
import { defineRouteConfig } from "@medusajs/admin-sdk"

const CustomPage = () => {
  return <div>Page content</div>
}

export const config = defineRouteConfig({
  label: "Custom Page",
})

export default CustomPage

Browser tab title (v2.17.2+): by default a UI route's tab title is its label. Export a handle with an seo resolver to override it, including dynamically from the route's loader data:

// src/admin/routes/brands/[id]/page.tsx
import { UIMatch } from "react-router-dom"

export const handle = {
  seo: (match: UIMatch<BrandResponse>) => ({
    title: match.loaderData?.brand.name || "Brand",
  }),
}

If seo returns no title, the dashboard falls back to the sidebar label, then the breadcrumb, then Medusa.

Custom injection zones (v2.16.0+): custom pages — most usefully in plugins — can expose their own widget injection zones by laying the page out with LayoutComposer from @medusajs/dashboard/components:

import { LayoutComposer } from "@medusajs/dashboard/components"

const BrandDetailsPage = () => (
  <LayoutComposer
    widgetsZonePrefix="brand.details"   // exposes "brand.details" and "brand.details.side"
    preferredLayoutId="core:two-column"
    data={brand}                        // passed to widgets as their `data` prop
    sections={{ main: <GeneralSection brand={brand} />, side: <MediaSection brand={brand} /> }}
  />
)
  • Name zones {resource}.{page-context} (e.g. brand.list, brand.details), plus .side for the side section. Never add .before/.after.
  • Register the zones in the InjectionZoneRegistry interface for type checking and autocompletion in defineWidgetConfig, and include "../../.medusa/types/augmentation-refs.d.ts" in src/admin/tsconfig.json's include array.
  • A build warning about an unknown zone is expected for custom zones — verify the zone name is spelled correctly rather than ignoring it blindly.

Common Issues & Solutions

"Cannot find module" errors (pnpm users):

  • Install peer dependencies BEFORE coding
  • Use exact versions from dashboard

"No QueryClient set" error:

  • pnpm: Install @tanstack/react-query
  • npm/yarn: Remove incorrectly installed package

"DataTable.Search not enabled":

  • Must pass search configuration to useDataTable

Widget not refreshing:

  • Invalidate display queries, not just modal queries
  • Include all dependencies in query keys

Display empty on refresh:

  • Display query has conditional enabled based on UI state
  • Remove condition - display data must load on mount

Next Steps - Testing Your Implementation

After successfully implementing a feature, always provide these next steps to the user:

1. Start the Development Server

If the server isn't already running, start it:

npm run dev      # or pnpm dev / yarn dev

2. Access the Admin Dashboard

Open your browser and navigate to:

Log in with your admin credentials.

3. Navigate to Your Custom UI

For Widgets: Navigate to the page where your widget is displayed. Common widget zones:

  • Product widgets: Go to Products → Select a product → Your widget appears on the page
  • Order widgets: Go to Orders → Select an order → Your widget appears on the page
  • Customer widgets: Go to Customers → Select a customer → Your widget appears on the page

Its exact position within the page is controlled by the user in the dashboard's Editor view (Layout Composer), not by the zone's .before/.after suffix. Tell the user they can drag the widget where they want it.

For UI Routes (Custom Pages):

  • Look for your custom page in the admin sidebar/navigation (based on the label you configured)
  • Or navigate directly to: http://localhost:9000/app/[your-route-path]

4. Test Functionality

Depending on what was implemented, test:

  • Forms: Try creating/editing entities, verify validation and error messages
  • Tables: Test pagination, search, sorting, and row selection
  • Data display: Verify data loads correctly and refreshes after mutations
  • Modals: Open FocusModal/Drawer, test form submission, verify data updates
  • Navigation: Click links and verify routing works correctly

Format for Presenting Next Steps

Always present next steps in a clear, actionable format after implementation:

## Implementation Complete

The [feature name] has been successfully implemented. Here's how to see it:

### Start the Development Server
[command based on package manager]

### Access the Admin Dashboard
Open http://localhost:9000/app in your browser and log in.

### View Your Custom UI

**For Widgets:**
1. Navigate to [specific admin page, e.g., "Products"]
2. Select [an entity, e.g., "any product"]
3. Scroll to [zone location, e.g., "the bottom of the page"]
4. You'll see your "[widget name]" widget

**For UI Routes:**
1. Look for "[page label]" in the admin navigation
2. Or navigate directly to http://localhost:9000/app/[route-path]

### What to Test
1. [Specific test case 1]
2. [Specific test case 2]
3. [Specific test case 3]

來自 medusajs 的更多技能

mcloud-variables
medusajs
執行 mcloud variables 指令,列出並取得雲端環境的環境變數。用於檢查、讀取或匯出環境…
official
building-storefronts
medusajs
以SDK為優先的前端整合方式,適用於Medusa商店前端,採用React Query模式並遵循關鍵API呼叫規則。所有API請求必須使用Medusa JS SDK,絕不能使用一般的fetch(),因為它缺少必要的標頭(商店路由需要可發布的API金鑰,管理路由需要驗證資訊)。傳遞純JavaScript物件給SDK方法,切勿對主體參數使用JSON.stringify(),因為SDK會自動處理序列化。使用useQuery處理GET請求,使用useMutation處理POST/DELETE請求...
official
learning-medusa
medusajs
互動式逐步Medusa開發訓練營,在建立品牌功能的同時學習架構模式。三個漸進式課程(總計2-3小時),涵蓋模組、工作流程、API路由、模組連結、工作流程鉤子及管理後台UI自訂。每個主要元件完成後設有檢查點驗證,測試概念理解、程式碼品質與功能正確性後才繼續進行。將錯誤視為教學機會,透過診斷問題與根本原因分析共同除錯...
official
db-migrate
medusajs
執行待處理的 Medusa 資料庫遷移並回報結果。透過 Bash 執行 npx medusa db:migrate 以將所有待處理的遷移套用至 Medusa 資料庫。回報遷移結果,包括已套用的遷移數量、遇到的任何錯誤以及成功確認。專為使用標準 npm/npx 設定的 Medusa 專案設計。
official
mcloud-environments
medusajs
執行 mcloud environments 指令,以列出、取得、建立、刪除、重新部署或觸發雲端環境的建置。適用於管理環境生命週期等情境。
official
db-generate
medusajs
以單一指令為 Medusa 模組生成資料庫遷移檔案。封裝 npx medusa db:generate CLI 指令,為指定的 Medusa 模組建立遷移檔案。接受模組名稱作為參數,並回報遷移檔案位置、錯誤及後續步驟。自動建議在生成後執行 npx medusa db:migrate 以套用遷移。
official
mcloud-deployments
medusajs
執行 mcloud deployments 指令以列出部署、取得部署詳細資訊,並擷取建置日誌。用於列出部署、檢查部署…
official
building-with-medusa
medusajs
Medusa後端架構、工作流程及關鍵實作規則的全面指南。涵蓋六大規則類別(架構、型別安全、商業邏輯放置、匯入、資料存取、檔案組織),包含具體的反模式與強制檢查。嚴格執行分層分離:模組 → 工作流程 → API路由 → 前端,所有變更操作必須透過工作流程,且僅允許GET/POST/DELETE HTTP方法。包含關鍵資料處理規則:價格按原樣儲存...
official