From medusa-dev
Provides patterns for Medusa Admin dashboard UI customizations: widgets, custom pages, forms, tables, data loading, and navigation. Essential for planning and implementation.
npx claudepluginhub adaptive-machines/medusa-agent-skills --plugin medusa-devThis skill uses the workspace's default tool permissions.
Build custom UI extensions for the Medusa Admin dashboard using the Admin SDK and Medusa UI components.
Creates isolated Git worktrees for feature branches with prioritized directory selection, gitignore safety checks, auto project setup for Node/Python/Rust/Go, and baseline verification.
Executes implementation plans in current session by dispatching fresh subagents per independent task, with two-stage reviews: spec compliance then code quality.
Dispatches parallel agents to independently tackle 2+ tasks like separate test failures or subsystems without shared state or dependencies.
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).
Load this skill for ANY admin UI development task, including:
Also load these skills when:
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:
references/data-loading.md firstreferences/forms.md firstreferences/display-patterns.md firstreferences/table-selection.md firstreferences/navigation.md firstreferences/typography.md firstMinimum requirement: Load at least 1-2 reference files relevant to your specific task before implementing.
⚠️ CRITICAL: This skill should be consulted FIRST for planning and implementation.
Use this skill for (PRIMARY SOURCE):
Use MedusaDocs MCP server for (SECONDARY SOURCE):
Why skills come first:
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",
},
})
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.
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Data Loading | CRITICAL | data- |
| 2 | Design System | CRITICAL | design- |
| 3 | Data Display | HIGH (includes CRITICAL price rule) | display- |
| 4 | Typography | HIGH | typo- |
| 5 | Forms & Modals | MEDIUM | form- |
| 6 | Selection Patterns | MEDIUM | select- |
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 routesdata-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 queriesdata-invalidate-display - Invalidate display queries after mutations, not just modal queriesdata-loading-states - Always show loading states (Spinner), not empty statesdata-pnpm-install-first - pnpm users MUST install @tanstack/react-query BEFORE codingdesign-semantic-colors - Always use semantic color classes (bg-ui-bg-base, text-ui-fg-subtle), never hardcodeddesign-spacing - Use px-6 py-4 for section padding, gap-2 for lists, gap-3 for itemsdesign-button-size - Always use size="small" for buttons in widgets and tablesdesign-medusa-components - Always use Medusa UI components (Container, Button, Text), not raw HTMLdisplay-price-format - CRITICAL: Prices from Medusa are stored as-is ($49.99 = 49.99, NOT in cents). Display them directly - NEVER divide by 100typo-text-component - Always use Text component from @medusajs/ui, never plain span/p tagstypo-labels - Use <Text size="small" leading="compact" weight="plus"> for labels/headingstypo-descriptions - Use <Text size="small" leading="compact" className="text-ui-fg-subtle"> for descriptionstypo-no-heading-widgets - Never use Heading for small sections in widgets (use Text instead)form-focusmodal-create - Use FocusModal for creating new entitiesform-drawer-edit - Use Drawer for editing existing entitiesform-disable-pending - Always disable actions during mutations (disabled={mutation.isPending})form-show-loading - Show loading state on submit button (isLoading={mutation.isPending})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" errorALWAYS 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:
Before implementing, verify you're NOT doing these:
Data Loading:
Design System:
Data Display:
Typography:
Forms:
Selection:
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:
⚠️ 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:
Authorization and session cookie headersx-publishable-api-key headerWhen to use what:
sdk.admin.product.list(), sdk.store.product.list())sdk.client.fetch() for your custom API routesFor implementing backend API routes, load the building-with-medusa skill.
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.after",
})
export default MyWidget
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
"Cannot find module" errors (pnpm users):
"No QueryClient set" error:
"DataTable.Search not enabled":
Widget not refreshing:
Display empty on refresh:
enabled based on UI stateAfter successfully implementing a feature, always provide these next steps to the user:
If the server isn't already running, start it:
npm run dev # or pnpm dev / yarn dev
Open your browser and navigate to:
Log in with your admin credentials.
For Widgets: Navigate to the page where your widget is displayed. Common widget zones:
product.details.after)For UI Routes (Custom Pages):
label you configured)http://localhost:9000/app/[your-route-path]Depending on what was implemented, test:
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]