building-admin-dashboard-customizations について
Medusa AdminダッシュボードのカスタムUI拡張を開発するためのスキルです。ウィジェットや管理ページに加え、Medusa SDKによるデータ取得、React Queryのパターン、実装リファレンスを扱います。
主な機能
- 01ダッシュボードのウィジェットとページ作成
- 02フォームとモーダルの作成
- 03テーブルやリストでのデータ表示
- 04Medusa SDKによるAPIアクセスパターン
- 05デザインシステムとコンポーネントの指針
活用シーン
- 01商品または注文ページにウィジェットを追加
- 02カスタム管理ページを作成
- 03バックエンドのデータを読み書きするフォームを構築
MCP 対応
このスキルは実装パターンと計画を扱い、MedusaDocs MCP Serverはコンポーネントのプロパティ、ウィジェットゾーン、SDKメソッドの詳細確認に使うと説明されています。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ medusajs/ medusa-agent-skills --skill building-admin-dashboard-customizationsインストール後、Claude Code で/building-admin-dashboard-customizationsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/data-loading.mddisplay-patterns.mdforms.mdnavigation.mdtable-selection.mdtypography.md詳細
Medusa 管理ダッシュボードのカスタマイズ
Admin SDK と Medusa UI コンポーネントを使用して、Medusa Admin ダッシュボードのカスタム UI 拡張機能を作成します。
注:「UI ルート」はカスタムの管理ページであり、バックエンド API ルートとは異なります(バックエンド API ルートには building-with-medusa スキルを使用します)。
使用するタイミング
次のような管理 UI の開発タスクでは、必ずこのスキルを読み込んでください。
- 商品/注文/顧客ページのウィジェットを作成する
- カスタム管理ページを構築する
- フォームやモーダルを実装する
- テーブルやリストでデータを表示する
- ページ間のナビゲーションを追加する
次の場合は、次のスキルも読み込んでください。
- building-with-medusa: 管理 UI が呼び出すバックエンド API ルートを構築する場合
- building-storefronts: 管理ダッシュボードではなく、ストアフロントを扱う場合
重要:必要に応じてリファレンスファイルを読み込む
以下のクイックリファレンスだけでは実装に不十分です。 該当するコンポーネントのコードを書く前に、関連するリファレンスファイルを必ず読み込んでください。
実装内容に応じて、次のリファレンスを読み込んでください。
- ウィジェットを作成する場合は? → まず
references/data-loading.mdを必ず読み込む - フォーム/モーダルを構築する場合は? → まず
references/forms.mdを必ず読み込む - テーブル/リストでデータを表示する場合は? → まず
references/display-patterns.mdを必ず読み込む - 大規模なデータセットから選択する場合は? → まず
references/table-selection.mdを必ず読み込む - ナビゲーションを追加する場合は? → まず
references/navigation.mdを必ず読み込む - コンポーネントにスタイルを適用する場合は? → まず
references/typography.mdを必ず読み込む
最低要件: 実装前に、具体的なタスクに関連するリファレンスファイルを少なくとも 1~2 個読み込んでください。
このスキルと MedusaDocs MCP サーバーの使い分け
⚠️ 重要:計画と実装では、まずこのスキルを参照してください。
次の用途には、このスキルを使用します(主な情報源):
- 計画 - 管理 UI 機能の構成方法を理解する
- コンポーネントのパターン - ウィジェット、ページ、フォーム、テーブル、モーダル
- デザインシステム - タイポグラフィ、色、間隔、セマンティッククラス
- データ読み込み - 重要な個別クエリのパターン、キャッシュの無効化
- ベストプラクティス - 正しいパターンと誤ったパターン(例:マウント時の表示クエリ)
- 重要なルール - 避けるべきこと(条件付き表示クエリなど、よくある間違い)
次の用途には、MedusaDocs MCP サーバーを使用します(補助的な情報源):
- 使用するコンポーネントが分かった後の、個別コンポーネントのプロパティのシグネチャ
- 利用可能なウィジェットゾーンの一覧
- JS SDK メソッドの詳細
- 設定オプションのリファレンス
スキルを先に使う理由:
- スキルには、MCP ではあまり強調されない、表示クエリとモーダルクエリを分けるなどの重要なパターンが含まれています
- スキルには正しいパターンと誤ったパターンが示されていますが、MCP では可能なことが示されています
- 計画には API リファレンスだけでなく、パターンの理解が必要です
重要なセットアップルール
SDK クライアントの設定
重要: 設定は必ず正確に使用してください。値が異なるとエラーが発生します。
// 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 ユーザーのみ
重要: コードを書く前に、ピア依存関係をインストールしてください。
# 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 ユーザー: これらのパッケージはすでに利用可能なため、インストールしないでください。
優先度別のルールカテゴリ
| 優先度 | カテゴリ | 影響 | プレフィックス |
|---|---|---|---|
| 1 | データ読み込み | 重大 | data- |
| 2 | デザインシステム | 重大 | design- |
| 3 | データ表示 | 高(重大な価格ルールを含む) | display- |
| 4 | タイポグラフィ | 高 | typo- |
| 5 | フォームとモーダル | 中 | form- |
| 6 | 選択パターン | 中 | select- |
クイックリファレンス
1. データの読み込み(重要)
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-query をインストールします
2. デザインシステム(重要)
design-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- 生の HTML ではなく、必ず Medusa UI コンポーネント(Container、Button、Text)を使用します
3. データ表示(高)
display-price-format- 重要:Medusa の価格はそのまま保存されます($49.99 = 49.99 であり、セント単位ではありません)。そのまま表示してください。100 で割るのは絶対にやめてください
4. タイポグラフィ(高)
typo-text-component- プレーンな span/p タグではなく、必ず @medusajs/ui の Text コンポーネントを使用します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 を使用します)
5. フォームとモーダル(中)
form-focusmodal-create- 新しいエンティティの作成には FocusModal を使用しますform-drawer-edit- 既存のエンティティの編集には Drawer を使用しますform-disable-pending- ミューテーション中は必ずアクションを無効にします(disabled={mutation.isPending})form-show-loading- 送信ボタンに読み込み状態を表示します(isLoading={mutation.isPending})
6. 選択パターン(中)
select-small-datasets- 選択肢が 2~10 個(ステータス、タイプなど)の場合は Select コンポーネントを使用しますselect-large-datasets- 選択肢が多い場合(商品、カテゴリなど)は FocusModal と DataTable を使用しますselect-search-config- 「search not enabled」エラーを避けるため、useDataTable に検索設定を必ず渡します
重要なデータ読み込みパターン
必ずこのパターンに従い、表示データを条件付きで読み込まないでください。
// ✅ 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!
}
重要な理由:
- ページを更新するとモーダルが閉じているため、条件付きクエリは実行されません
- ユーザーにはデータではなく空の状態が表示されます
- 表示がモーダルの操作に依存してしまいます(UX が損なわれます)
よくある間違いのチェックリスト
実装する前に、以下のことをしていないか確認してください。
データの読み込み:
- Medusa JS SDK ではなく通常の fetch() を使用している(認証ヘッダーがないというエラーの原因になります)
- 組み込みエンドポイントに既存の SDK メソッドを使用していない(例:sdk.admin.product.list() ではなく sdk.client.fetch("/admin/products") を使用している)
- モーダルや UI の状態に応じて表示データを条件付きで読み込んでいる
- 表示用とモーダル用に単一のクエリを使用している
- ミューテーション後に表示用クエリを無効化し忘れている
- 読み込み状態を処理していない(スピナーではなく空の表示にしている)
- pnpm を使用している場合:コーディング前に @tanstack/react-query をインストールしていない
デザインシステム:
- セマンティッククラスではなく、色をハードコードしている
- ウィジェット内のボタンに size="small" を指定し忘れている
- セクションのパディングに px-6 py-4 を使用していない
- Medusa UI コンポーネントではなく、生の HTML 要素を使用している
データ表示:
- 重大:表示時に価格を 100 で割っている(価格はそのまま保存されています:$49.99 = 49.99 であり、セント単位ではありません)
タイポグラフィ:
- Text コンポーネントではなく、通常の span/p タグを使用している
- ラベルに weight="plus" を使用していない
- 説明に text-ui-fg-subtle を使用していない
- 小さなウィジェットセクションで Heading を使用している
フォーム:
- 作成に Drawer を使用している(FocusModal を使用してください)
- 編集に FocusModal を使用している(Drawer を使用してください)
- ミューテーション中にボタンを無効化していない
- 送信時に読み込み状態を表示していない
選択:
- 10 件未満の項目に DataTable を使用している(過剰です)
- 10 件を超える項目に Select を使用している(UX がよくありません)
- useDataTable で検索を設定していない(エラーの原因になります)
利用可能なリファレンスファイル
詳しい実装パターンについては、以下を読み込んでください。
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
各リファレンスには以下が含まれます。
- 実装手順のガイド
- 正しいコード例と誤ったコード例
- よくある間違いと解決策
- 完全に動作するサンプル
バックエンドとの連携
⚠️ 重大:すべての API リクエストには必ず Medusa JS SDK を使用してください。通常の fetch() は絶対に使用しないでください。
管理 UI は SDK を使用してバックエンドの API ルートに接続します。
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")
},
})
SDK が必要な理由:
- 管理ルートでは
Authorizationヘッダーとセッション Cookie ヘッダーが必要です - ストアルートでは
x-publishable-api-keyヘッダーが必要です - SDK は必要なすべてのヘッダーを自動的に処理します
- ヘッダーなしの通常の fetch() → 認証/認可エラー
- 既存の SDK メソッドを使用すると、型安全性が向上します
用途に応じた使い分け:
- 組み込みエンドポイント:既存の SDK メソッド(
sdk.admin.product.list()、sdk.store.product.list())を使用します - カスタムエンドポイント:カスタム API ルートには
sdk.client.fetch()を使用します
バックエンド API ルートを実装する場合は、building-with-medusa スキルを読み込んでください。
ウィジェットと UI ルート
ウィジェットは既存の管理ページを拡張します:
// 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 は配置を制御しなくなりました(v2.17.2+):
Layout Composer の導入以降、管理者はダッシュボードのエディタービューでウィジェットなどのコンポーネントを配置し、その配置はデータベースに保存されます。.before と .after のゾーンサフィックスは非推奨です。product.details.before のウィジェットと product.details.after のウィジェットは同じ挿入ゾーンに配置され、最終的な順序はユーザーの設定によって決まります。
- サフィックスを根拠に、特定の位置に表示されるとユーザーに約束しないでください。「ウィジェットは商品詳細ページに表示され、エディタービューで位置を変更できます」と伝えてください。
.sideは引き続き意味があり、2 列のページレイアウトのサイド列を対象とします。- 新しいウィジェットでは、プロジェクトですでにサフィックスを標準化している場合を除き、サフィックスなしのゾーン(例:
product.details)を優先してください。
v2.16.0 で追加された新しいゾーンは、下書き注文、ギフトカード、ストアクレジットアカウント(draft_order.*、gift_card.*、store_credit_account.* の details/list/side バリアント)を対象とします。ゾーン名を推測せず、正確なゾーン一覧については MedusaDocs MCP サーバーに問い合わせてください。
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
ブラウザータブのタイトル(v2.17.2+): デフォルトでは、UI ルートのタブタイトルに label が使われます。seo リゾルバーを持つ handle をエクスポートすると、ルートの loader データを使って動的に上書きすることもできます:
// 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",
}),
}
seo がタイトルを返さない場合、ダッシュボードはサイドバーのラベル、パンくずリスト、Medusa の順にフォールバックします。
カスタム挿入ゾーン(v2.16.0+): カスタムページ(特にプラグインで便利です)では、ページレイアウトに @medusajs/dashboard/components の LayoutComposer を使用することで、独自のウィジェット挿入ゾーンを公開できます:
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} /> }}
/>
)
- ゾーン名は
{resource}.{page-context}(例:brand.list、brand.details)とし、サイドセクションには.sideを付けます。.before/.afterは追加しないでください。 - 型チェックと
defineWidgetConfigでの自動補完のために、ゾーンをInjectionZoneRegistryインターフェースに登録し、src/admin/tsconfig.jsonのinclude配列に"../../.medusa/types/augmentation-refs.d.ts"を含めてください。 - カスタムゾーンでは、不明なゾーンに関するビルド警告が出るのは想定どおりです。むやみに無視せず、ゾーン名のつづりが正しいことを確認してください。
よくある問題と解決策
「Cannot find module」エラー(pnpm ユーザー):
- コーディングを始める前にピア依存関係をインストールする
- ダッシュボードで使用されている正確なバージョンを使う
「No QueryClient set」エラー:
- pnpm:@tanstack/react-query をインストールする
- npm/yarn:誤ってインストールされたパッケージを削除する
「DataTable.Search not enabled」:
- useDataTable に検索設定を渡す必要があります
ウィジェットが更新されない:
- モーダルのクエリだけでなく、表示用クエリを無効化する
- クエリキーにすべての依存関係を含める
更新後に表示が空になる:
- UI の状態に応じて、表示用クエリの
enabledが条件付きになっている - 条件を削除する。表示データはマウント時に読み込む必要があります
次のステップ - 実装のテスト
機能の実装が正常に完了したら、必ず次のステップをユーザーに案内してください:
1. 開発サーバーを起動する
サーバーがまだ起動していない場合は、起動します:
npm run dev # or pnpm dev / yarn dev
2. 管理ダッシュボードにアクセスする
ブラウザーを開き、次の URL にアクセスします:
- 管理ダッシュボード: http://localhost:9000/app
管理者の認証情報でログインします。
3. カスタム UI に移動する
ウィジェットの場合: ウィジェットが表示されるページに移動します。一般的なウィジェットゾーン:
- 商品ウィジェット: Products → 商品を選択 → ページにウィジェットが表示されます
- 注文ウィジェット: Orders → 注文を選択 → ページにウィジェットが表示されます
- 顧客ウィジェット: Customers → 顧客を選択 → ページにウィジェットが表示されます
ページ内の正確な配置は、ゾーンの .before/.after サフィックスではなく、ダッシュボードの Editor ビュー(Layout Composer)でユーザーが設定します。ウィジェットは好きな位置にドラッグできることをユーザーに伝えてください。
UI ルート(カスタムページ)の場合:
- 管理画面のサイドバー/ナビゲーションでカスタムページを探します(設定した
labelに基づきます) - または、次の URL に直接移動します:
http://localhost:9000/app/[your-route-path]
4. 機能をテストする
実装した内容に応じて、次の項目をテストします:
- フォーム: エンティティの作成/編集を試し、検証とエラーメッセージを確認します
- テーブル: ページネーション、検索、並べ替え、行選択をテストします
- データ表示: データが正しく読み込まれ、変更後に更新されることを確認します
- モーダル: FocusModal/Drawer を開き、フォームの送信をテストして、データが更新されることを確認します
- ナビゲーション: リンクをクリックし、ルーティングが正しく動作することを確認します
次のステップの提示形式
実装後は、次のステップを常に明確で実行しやすい形式で提示します:
## 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]