building-admin-dashboard-customizations

작성자: medusajs

Medusa Admin 대시보드용 맞춤 UI 확장 기능으로, Admin SDK와 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 명령어를 실행하여 Cloud 환경의 환경 변수를 나열하고 가져옵니다. 환경을 검사하거나 읽거나 내보낼 때 사용합니다.
official
building-storefronts
medusajs
SDK 기반의 Medusa 스토어프론트 통합으로, React Query 패턴과 중요한 API 호출 규칙을 포함합니다. 모든 API 요청에는 항상 Medusa JS SDK를 사용해야 하며, 일반 fetch()는 사용하지 않습니다. fetch()는 필수 헤더(스토어 라우트의 publishable API 키, 관리자 라우트의 인증)가 누락되기 때문입니다. SDK 메서드에는 일반 JavaScript 객체를 전달하며, 본문 매개변수에 JSON.stringify()를 사용하지 않습니다. SDK가 자동으로 직렬화를 처리합니다. GET 요청에는 useQuery를, POST/DELETE 요청에는 useMutation을 사용합니다.
official
learning-medusa
medusajs
대화형 단계별 메두사 개발 부트캠프로, 브랜드 기능을 구축하면서 아키텍처 패턴을 학습합니다. 모듈, 워크플로우, API 라우트, 모듈 링크, 워크플로우 훅, 관리자 UI 커스터마이징을 다루는 3개의 점진적 레슨(총 2~3시간)으로 구성됩니다. 각 주요 구성 요소 이후 체크포인트 검증을 통해 개념 이해도, 코드 품질, 기능성을 확인한 후 진행합니다. 오류를 교육 기회로 활용하며, 진단 질문과 근본 원인 분석을 통해 함께 디버깅합니다...
official
db-migrate
medusajs
보류 중인 Medusa 데이터베이스 마이그레이션을 실행하고 결과를 보고합니다. Bash를 통해 npx medusa db:migrate를 실행하여 보류 중인 모든 마이그레이션을 Medusa 데이터베이스에 적용합니다. 적용된 마이그레이션 수, 발생한 오류, 성공 확인을 포함한 마이그레이션 결과를 보고합니다. 표준 npm/npx 설정을 사용하는 Medusa 프로젝트용으로 설계되었습니다.
official
mcloud-environments
medusajs
mcloud environments 명령을 실행하여 Cloud 환경을 나열, 조회, 생성, 삭제, 재배포 또는 빌드를 트리거합니다. 환경 수명 주기를 관리할 때 사용합니다.
official
db-generate
medusajs
단일 명령어로 Medusa 모듈의 데이터베이스 마이그레이션을 생성합니다. npx medusa db:generate CLI 명령을 래핑하여 지정된 Medusa 모듈의 마이그레이션 파일을 생성합니다. 모듈 이름을 인수로 받아 마이그레이션 파일 위치, 오류 및 다음 단계를 보고합니다. 생성 후 마이그레이션을 적용하기 위해 npx medusa db:migrate를 실행하도록 자동으로 제안합니다.
official
mcloud-deployments
medusajs
Execute mcloud deployments commands to list deployments, retrieve deployment details, and fetch build logs. Use when listing deployments, checking deployment…
official
building-with-medusa
medusajs
Medusa 백엔드 아키텍처, 워크플로우 및 중요한 구현 규칙에 대한 종합 가이드입니다. 여섯 가지 규칙 범주(아키텍처, 타입 안전성, 비즈니스 로직 배치, 임포트, 데이터 접근, 파일 구성)를 다루며, 특정 안티패턴과 적용 확인 사항을 포함합니다. 엄격한 계층 분리(모듈 → 워크플로우 → API 라우트 → 프론트엔드)를 적용하며, 모든 변경 작업에 워크플로우를 요구하고 GET/POST/DELETE HTTP 메서드만 허용합니다. 가격을 있는 그대로 저장하는 등 중요한 데이터 처리 규칙을 포함합니다...
official