building-admin-dashboard-customizations by medusajs/medusa-agent-skills
npx skills add https://github.com/medusajs/medusa-agent-skills --skill building-admin-dashboard-customizations使用 Admin SDK 和 Medusa UI 组件为 Medusa 管理面板构建自定义 UI 扩展。
注意: "UI 路由" 指的是自定义的管理页面,不同于后端 API 路由(后者使用 building-with-medusa 技能)。
为任何管理 UI 开发任务加载此技能,包括:
在以下情况也加载这些技能:
下面的快速参考不足以用于实现。 在编写该组件的代码之前,您必须加载相关的参考文件。
根据您要实现的功能加载这些参考:
references/data-loading.mdreferences/forms.mdreferences/display-patterns.md广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
references/table-selection.mdreferences/navigation.mdreferences/typography.md最低要求: 在实现之前,至少加载 1-2 个与您具体任务相关的参考文件。
⚠️ 关键:规划和实现时应首先参考此技能。
将此技能用于(主要来源):
将 MedusaDocs MCP 服务器用于(次要来源):
为什么技能优先:
关键: 始终使用精确配置 - 不同的值会导致错误:
// 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 list @tanstack/react-query --depth=10 | grep @medusajs/dashboard
# 安装该确切版本
pnpm add @tanstack/react-query@[exact-version]
# 如果使用导航(Link 组件)
pnpm list react-router-dom --depth=10 | grep @medusajs/dashboard
pnpm add react-router-dom@[exact-version]
npm/yarn 用户: 不要安装这些包 - 它们已经可用。
| 优先级 | 类别 | 影响 | 前缀 |
|---|---|---|---|
| 1 | 数据加载 | 关键 | data- |
| 2 | 设计系统 | 关键 | design- |
| 3 | 数据显示 | 高(包括关键价格规则) | display- |
| 4 | 排版 | 高 | typo- |
| 5 | 表单和模态框 | 中 | form- |
| 6 | 选择模式 | 中 | select- |
data-sdk-always - 所有 API 请求始终使用 Medusa JS SDK - 切勿使用常规 fetch()(缺少身份验证标头会导致错误)data-sdk-method-choice - 对内置端点使用现有的 SDK 方法(sdk.admin.product.list()),对自定义路由使用 sdk.client.fetch()data-display-on-mount - 显示查询必须在挂载时加载(不能基于 UI 状态设置 enabled 条件)data-separate-queries - 将显示查询与模态框/表单查询分开data-invalidate-display - 变更后使显示查询失效,而不仅仅是模态框查询data-loading-states - 始终显示加载状态(Spinner),而不是空状态data-pnpm-install-first - pnpm 用户必须在编码前安装 @tanstack/react-querydesign-semantic-colors - 始终使用语义颜色类(bg-ui-bg-base, text-ui-fg-subtle),切勿硬编码design-spacing - 使用 px-6 py-4 作为区域内边距,gap-2 用于列表,gap-3 用于项目design-button-size - 小部件和表格中的按钮始终使用 size="small"design-medusa-components - 始终使用 Medusa UI 组件(Container, Button, Text),而不是原始 HTMLdisplay-price-format - 关键:来自 Medusa 的价格按原样存储($49.99 = 49.99,不是以分为单位)。直接显示它们 - 切勿除以 100typo-text-component - 始终使用来自 @medusajs/ui 的 Text 组件,切勿使用普通的 span/p 标签typo-labels - 使用 <Text size="small" leading="compact" weight="plus"> 作为标签/标题typo-descriptions - 使用 <Text size="small" leading="compact" className="text-ui-fg-subtle"> 作为描述typo-no-heading-widgets - 切勿在小部件的小节中使用 Heading(改用 Text)form-focusmodal-create - 使用 FocusModal 创建新实体form-drawer-edit - 使用 Drawer 编辑现有实体form-disable-pending - 在变更期间始终禁用操作(disabled={mutation.isPending})form-show-loading - 在提交按钮上显示加载状态(isLoading={mutation.isPending})select-small-datasets - 对 2-10 个选项使用 Select 组件(状态、类型等)select-large-datasets - 对大型数据集使用带有 FocusModal 的 DataTable(产品、类别等)select-search-config - 必须将搜索配置传递给 useDataTable 以避免 "search not enabled" 错误始终遵循此模式 - 切勿有条件地加载显示数据:
// ✅ 正确 - 具有适当职责的分开查询
const RelatedProductsWidget = ({ data: product }) => {
const [modalOpen, setModalOpen] = useState(false)
// 显示查询 - 在挂载时加载
const { data: displayProducts } = useQuery({
queryFn: () => fetchSelectedProducts(selectedIds),
queryKey: ["related-products-display", product.id],
// 没有 'enabled' 条件 - 立即加载
})
// 模态框查询 - 需要时加载
const { data: modalProducts } = useQuery({
queryFn: () => sdk.admin.product.list({ limit: 10, offset: 0 }),
queryKey: ["products-selection"],
enabled: modalOpen, // 对于仅模态框数据是可以的
})
// 具有适当失效功能的变更
const updateProduct = useMutation({
mutationFn: updateFunction,
onSuccess: () => {
// 使显示数据查询失效以刷新 UI
queryClient.invalidateQueries({ queryKey: ["related-products-display", product.id] })
// 同时使实体查询失效
queryClient.invalidateQueries({ queryKey: ["product", product.id] })
// 注意:不需要使模态框选择查询失效
},
})
return (
<Container>
{/* 显示使用 displayProducts */}
{displayProducts?.map(p => <div key={p.id}>{p.title}</div>)}
<FocusModal open={modalOpen} onOpenChange={setModalOpen}>
{/* 模态框使用 modalProducts */}
</FocusModal>
</Container>
)
}
// ❌ 错误 - 具有条件加载的单一查询
const BrokenWidget = ({ data: product }) => {
const [modalOpen, setModalOpen] = useState(false)
const { data } = useQuery({
queryFn: () => sdk.admin.product.list(),
enabled: modalOpen, // ❌ 页面刷新时显示中断!
})
// 尝试从模态框查询显示
const displayItems = data?.filter(item => ids.includes(item.id)) // 模态框打开前没有数据
return <div>{displayItems?.map(...)}</div> // 挂载时为空!
}
为什么这很重要:
在实现之前,请确认您没有做以下事情:
数据加载:
设计系统:
数据显示:
排版:
表单:
选择:
加载这些文件以获取详细模式:
references/data-loading.md - useQuery/useMutation 模式,缓存失效
references/forms.md - FocusModal/Drawer 模式,验证
references/table-selection.md - 完整的 DataTable 选择模式
references/display-patterns.md - 实体的列表、表格、卡片
references/typography.md - Text 组件模式
references/navigation.md - Link, useNavigate, useParams 模式
每个参考文件包含:
⚠️ 关键:所有 API 请求始终使用 Medusa JS SDK - 切勿使用常规 fetch()
管理 UI 使用 SDK 连接到后端 API 路由:
import { sdk } from "[LOCATE SDK INSTANCE IN PROJECT]"
// ✅ 正确 - 内置端点:使用现有的 SDK 方法
const { data: product } = useQuery({
queryKey: ["product", productId],
queryFn: () => sdk.admin.product.retrieve(productId),
})
// ✅ 正确 - 自定义端点:使用 sdk.client.fetch()
const { data: reviews } = useQuery({
queryKey: ["reviews", product.id],
queryFn: () => sdk.client.fetch(`/admin/products/${product.id}/reviews`),
})
// ❌ 错误 - 使用常规 fetch
const { data } = useQuery({
queryKey: ["reviews", product.id],
queryFn: () => fetch(`http://localhost:9000/admin/products/${product.id}/reviews`),
// ❌ 错误:缺少 Authorization 标头!
})
// 变更到自定义后端路由
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")
},
})
为什么需要 SDK:
Authorization 和会话 cookie 标头x-publishable-api-key 标头何时使用什么:
sdk.admin.product.list(), sdk.store.product.list())sdk.client.fetch()要实现后端 API 路由,请加载 building-with-medusa 技能。
小部件扩展现有的管理页面:
// 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 路由创建新的管理页面:
// 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" 错误(pnpm 用户):
"No QueryClient set" 错误:
"DataTable.Search not enabled":
小部件未刷新:
刷新时显示为空:
enabled成功实现功能后,始终向用户提供以下后续步骤:
如果服务器尚未运行,请启动它:
npm run dev # 或 pnpm dev / yarn dev
打开浏览器并导航到:
使用您的管理员凭据登录。
对于小部件: 导航到显示您小部件的页面。常见的小部件区域:
product.details.after)对于 UI 路由(自定义页面):
label)http://localhost:9000/app/[your-route-path]根据实现的内容,测试:
在实现后始终以清晰、可操作的格式呈现后续步骤:
## 实现完成
[功能名称] 已成功实现。以下是查看方法:
### 启动开发服务器
[基于包管理器的命令]
### 访问管理面板
在浏览器中打开 http://localhost:9000/app 并登录。
### 查看您的自定义 UI
**对于小部件:**
1. 导航到 [特定的管理页面,例如 "产品"]
2. 选择 [一个实体,例如 "任意产品"]
3. 滚动到 [区域位置,例如 "页面底部"]
4. 您将看到您的 "[小部件名称]" 小部件
**对于 UI 路由:**
1. 在管理导航中查找 "[页面标签]"
2. 或者直接导航到 http://localhost:9000/app/[路由路径]
### 要测试的内容
1. [具体测试用例 1]
2. [具体测试用例 2]
3. [具体测试用例 3]
每周安装
918
仓库
GitHub 星标
114
首次出现
2026年1月26日
安全审计
安装于
codex810
github-copilot803
opencode801
gemini-cli800
amp757
kimi-cli755
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 |
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]
Weekly Installs
918
Repository
GitHub Stars
114
First Seen
Jan 26, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex810
github-copilot803
opencode801
gemini-cli800
amp757
kimi-cli755
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
102,200 周安装
Gemini Interactions API 指南:统一接口、智能体交互与服务器端状态管理
833 周安装
Apollo MCP 服务器:让AI代理通过GraphQL API交互的完整指南
834 周安装
智能体记忆系统构建指南:分块策略、向量存储与检索优化
835 周安装
Scrapling官方网络爬虫框架 - 自适应解析、绕过Cloudflare、Python爬虫库
836 周安装
抽奖赢家选取器 - 随机选择工具,支持CSV、Excel、Google Sheets,公平透明
838 周安装
Medusa 前端开发指南:使用 SDK、React Query 构建电商商店
839 周安装
| MEDIUM |
form- |
| 6 | Selection Patterns | MEDIUM | select- |