mcpField
P
payload-admin-components

payload-admin-components

github.com/timschoch開発161 回インストール今日更新
リポジトリ

payload-admin-components について

Payload CMSの管理パネルにカスタムReactコンポーネントを追加するためのリファレンススキルです。全体、コレクション、グローバル文書、フィールドのUIをカスタマイズする開発者向けです。

主な機能

  1. 01管理パネルのルートコンポーネントをカスタマイズ
  2. 02コレクションとグローバルのコンポーネントを追加
  3. 03フィールドとセルのUIを作成
  4. 04コンポーネントのパスとPropsを設定
  5. 05サーバー/クライアントコンポーネントを利用
  6. 06Payload APIと翻訳にアクセス

活用シーン

  1. 01管理パネルのロゴやナビゲーションを置き換え
  2. 02ダッシュボードやヘッダーにアクションを追加
  3. 03フィールド編集と一覧表示をカスタマイズ
  4. 04サーバーコンポーネントでPayloadデータを取得

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/timschoch/skilly --skill payload-admin-components

インストール後、Claude Code で/payload-admin-componentsを入力して読み込まれたか確認します。

詳細

Payload CMS のカスタムコンポーネント

カスタムコンポーネントを使用すると、独自の React コンポーネントに差し替えて、管理パネルを完全にカスタマイズできます。インターフェースのほぼすべての部分を置き換えたり、新しい機能を追加したりできます。

コンポーネントの種類

カスタムコンポーネントには、主に次の 4 種類があります。

  1. ルートコンポーネント - 管理パネル全体に影響します(ロゴ、ナビゲーション、ヘッダー)
  2. コレクションコンポーネント - コレクションビュー専用です
  3. グローバルコンポーネント - グローバルドキュメントビュー専用です
  4. フィールドコンポーネント - カスタムフィールドの UI とセルです

カスタムコンポーネントの定義

コンポーネントのパス

設定を軽量に保ち、Node.js との互換性を確保するため、コンポーネントは直接インポートするのではなく、ファイルパスを使用して定義します。

import { buildConfig } from 'payload'

export default buildConfig({
  admin: {
    components: {
      logout: {
        Button: '/src/components/Logout#MyComponent', // Named export
      },
      Nav: '/src/components/Nav', // Default export
    },
  },
})

コンポーネントのパスのルール:

  1. パスはプロジェクトのルート(または config.admin.importMap.baseDir)からの相対パスです
  2. 名前付きエクスポートの場合:#ExportName を追加するか、exportName プロパティを使用します
  3. デフォルトエクスポートの場合:サフィックスは不要です
  4. ファイル拡張子は省略できます
コンポーネント設定オブジェクト

文字列のパスの代わりに、設定オブジェクトを渡すこともできます。

{
  logout: {
    Button: {
      path: '/src/components/Logout',
      exportName: 'MyComponent',
      clientProps: { customProp: 'value' },
      serverProps: { asyncData: someData },
    },
  },
}

設定プロパティ:

プロパティ説明
pathコンポーネントのファイルパス(名前付きエクスポートは # を使用)
exportName名前付きエクスポート(パス内の # の代わり)
clientPropsクライアントコンポーネントに渡す Props(シリアル化可能である必要があります)
serverPropsサーバーコンポーネントに渡す Props(シリアル化できない値も指定できます)
ベースディレクトリの設定
import path from 'path'
import { fileURLToPath } from 'node:url'

const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)

export default buildConfig({
  admin: {
    importMap: {
      baseDir: path.resolve(dirname, 'src'), // Set base directory
    },
    components: {
      Nav: '/components/Nav', // Now relative to src/
    },
  },
})

サーバーコンポーネントとクライアントコンポーネント

すべてのコンポーネントは、デフォルトで React サーバーコンポーネントです。

サーバーコンポーネント(デフォルト)

Local API を直接使用し、非同期処理を実行して、Payload インスタンス全体にアクセスできます。

import React from 'react'
import type { Payload } from 'payload'

async function MyServerComponent({ payload }: { payload: Payload }) {
  const page = await payload.findByID({
    collection: 'pages',
    id: '123',
  })

  return <p>{page.title}</p>
}

export default MyServerComponent
クライアントコンポーネント

インタラクティブ機能、フック、状態などを使用するには、'use client' ディレクティブを使用します。

'use client'
import React, { useState } from 'react'

export function MyClientComponent() {
  const [count, setCount] = useState(0)

  return <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
}

重要: クライアントコンポーネントは、シリアル化できない Props(関数、クラスインスタンスなど)を受け取れません。Payload はクライアントコンポーネントに渡す際に、これらを自動的に除外します。

デフォルトの Props

すべてのカスタムコンポーネントは、デフォルトで次の Props を受け取ります。

Prop説明型
payloadPayload インスタンス(Local API へのアクセス)Payload
i18n国際化オブジェクトI18n
locale現在のロケール(ローカライズが有効な場合)string

サーバーコンポーネントの例:

async function MyComponent({ payload, i18n, locale }) {
  const data = await payload.find({
    collection: 'posts',
    locale,
  })

  return <div>{data.docs.length} posts</div>
}

クライアントコンポーネントの例:

'use client'
import { usePayload, useLocale, useTranslation } from '@payloadcms/ui'

export function MyComponent() {
  // Access via hooks in client components
  const { getLocal, getByID } = usePayload()
  const locale = useLocale()
  const { t, i18n } = useTranslation()

  return <div>{t('myKey')}</div>
}

カスタム Props

clientProps または serverProps を使用して、追加の Props を渡します。

{
  logout: {
    Button: {
      path: '/components/Logout',
      clientProps: {
        buttonText: 'Sign Out',
        onLogout: () => console.log('Logged out'),
      },
    },
  },
}

コンポーネントで受け取ります。

'use client'
export function Logout({ buttonText, onLogout }) {
  return <button onClick={onLogout}>{buttonText}</button>
}

ルートコンポーネント

ルートコンポーネントは、管理パネル全体に影響します。

利用可能なルートコンポーネント
コンポーネント説明設定パス
Navナビゲーションサイドバー全体admin.components.Nav
graphics.Icon小さなアイコン(ナビゲーションで使用)admin.components.graphics.Icon
graphics.Logoロゴ全体(ログイン画面で使用)admin.components.graphics.Logo
logout.Buttonログアウトボタンadmin.components.logout.Button
actionsヘッダーアクション(配列)admin.components.actions
headerヘッダー上部(配列)admin.components.header
beforeDashboardダッシュボードコンテンツの前(配列)admin.components.beforeDashboard
afterDashboardダッシュボードコンテンツの後(配列)admin.components.afterDashboard
beforeLoginログインフォームの前(配列)admin.components.beforeLogin
afterLoginログインフォームの後(配列)admin.components.afterLogin
beforeNavLinksナビゲーションリンクの前(配列)admin.components.beforeNavLinks
afterNavLinksナビゲーションリンクの後(配列)admin.components.afterNavLinks
settingsMenu設定メニュー項目(配列)admin.components.settingsMenu
providersカスタム React Context プロバイダーadmin.components.providers
viewsカスタムビュー(ダッシュボードなど)admin.components.views
例:カスタムロゴ
export default buildConfig({
  admin: {
    components: {
      graphics: {
        Logo: '/components/Logo',
        Icon: '/components/Icon',
      },
    },
  },
})
// components/Logo.tsx
export default function Logo() {
  return <img src="/logo.png" alt="My Brand" width={200} />
}
例:ヘッダーアクション
export default buildConfig({
  admin: {
    components: {
      actions: ['/components/ClearCacheButton', '/components/PreviewButton'],
    },
  },
})
// components/ClearCacheButton.tsx
'use client'
export default function ClearCacheButton() {
  return (
    <button
      onClick={async () => {
        await fetch('/api/clear-cache', { method: 'POST' })
        alert('Cache cleared!')
      }}
    >
      Clear Cache
    </button>
  )
}

コレクションコンポーネント

コレクションコンポーネントは、コレクションのビューに固有のものです。

import type { CollectionConfig } from 'payload'

export const Posts: CollectionConfig = {
  slug: 'posts',
  admin: {
    components: {
      // Edit view components
      edit: {
        PreviewButton: '/components/PostPreview',
        SaveButton: '/components/CustomSave',
        SaveDraftButton: '/components/CustomSaveDraft',
        PublishButton: '/components/CustomPublish',
      },

      // List view components
      list: {
        Header: '/components/PostsListHeader',
        beforeList: ['/components/ListFilters'],
        afterList: ['/components/ListFooter'],
      },
    },
  },
  fields: [
    // ...
  ],
}

グローバルコンポーネント

コレクションコンポーネントと似ていますが、グローバルドキュメント向けです。

import type { GlobalConfig } from 'payload'

export const Settings: GlobalConfig = {
  slug: 'settings',
  admin: {
    components: {
      edit: {
        PreviewButton: '/components/SettingsPreview',
        SaveButton: '/components/SettingsSave',
      },
    },
  },
  fields: [
    // ...
  ],
}

フィールドコンポーネント

編集ビューとリストビューでのフィールドのレンダリング方法をカスタマイズします。

フィールドコンポーネント(編集ビュー)
{
  name: 'status',
  type: 'select',
  options: ['draft', 'published'],
  admin: {
    components: {
      Field: '/components/StatusField',
    },
  },
}
// components/StatusField.tsx
'use client'
import { useField } from '@payloadcms/ui'
import type { SelectFieldClientComponent } from 'payload'

export const StatusField: SelectFieldClientComponent = ({ path, field }) => {
  const { value, setValue } = useField({ path })

  return (
    <div>
      <label>{field.label}</label>
      <select value={value} onChange={(e) => setValue(e.target.value)}>
        {field.options.map((option) => (
          <option key={option.value} value={option.value}>
            {option.label}
          </option>
        ))}
      </select>
    </div>
  )
}
セルコンポーネント(リストビュー)
{
  name: 'status',
  type: 'select',
  options: ['draft', 'published'],
  admin: {
    components: {
      Cell: '/components/StatusCell',
    },
  },
}
// components/StatusCell.tsx
import type { SelectFieldCellComponent } from 'payload'

export const StatusCell: SelectFieldCellComponent = ({ data, cellData }) => {
  const isPublished = cellData === 'published'

  return (
    <span
      style={{
        color: isPublished ? 'green' : 'orange',
        fontWeight: 'bold',
      }}
    >
      {cellData}
    </span>
  )
}
UI フィールド(表示専用)

データに影響を与えずにカスタム UI を追加するための特殊なフィールドタイプです。

{
  name: 'refundButton',
  type: 'ui',
  admin: {
    components: {
      Field: '/components/RefundButton',
    },
  },
}
// components/RefundButton.tsx
'use client'
import { useDocumentInfo } from '@payloadcms/ui'

export default function RefundButton() {
  const { id } = useDocumentInfo()

  return (
    <button
      onClick={async () => {
        await fetch(`/api/orders/${id}/refund`, { method: 'POST' })
        alert('Refund processed')
      }}
    >
      Process Refund
    </button>
  )
}

フックの使用

Payload は、クライアントコンポーネント向けに多くの React フックを提供します。

'use client'
import {
  useAuth, // Current user
  useConfig, // Payload config (client-safe)
  useDocumentInfo, // Current document info (id, slug, etc.)
  useField, // Field value and setValue
  useForm, // Form state and dispatch
  useFormFields, // Multiple field values (optimized)
  useLocale, // Current locale
  useTranslation, // i18n translations
  usePayload, // Local API methods
} from '@payloadcms/ui'

export function MyComponent() {
  const { user } = useAuth()
  const { config } = useConfig()
  const { id, collection } = useDocumentInfo()
  const locale = useLocale()
  const { t } = useTranslation()

  return <div>Hello {user?.email}</div>
}

重要: これらのフックは、Admin Panel のコンテキスト内にあるクライアントコンポーネントでのみ動作します。

Payload 設定へのアクセス

サーバーコンポーネントの場合:

async function MyServerComponent({ payload }) {
  const { config } = payload
  return <div>{config.serverURL}</div>
}

クライアントコンポーネントの場合:

'use client'
import { useConfig } from '@payloadcms/ui'

export function MyClientComponent() {
  const { config } = useConfig() // Client-safe config
  return <div>{config.serverURL}</div>
}

重要: クライアントコンポーネントには、シリアライズ可能な形式の設定が渡されます(関数やバリデーションなどは取り除かれます)。

フィールド設定へのアクセス

サーバーコンポーネント:

import type { TextFieldServerComponent } from 'payload'

export const MyFieldComponent: TextFieldServerComponent = ({ field }) => {
  return <div>Field name: {field.name}</div>
}

クライアントコンポーネント:

'use client'
import type { TextFieldClientComponent } from 'payload'

export const MyFieldComponent: TextFieldClientComponent = ({ clientField }) => {
  // clientField has non-serializable props removed
  return <div>Field name: {clientField.name}</div>
}

翻訳(i18n)

サーバーコンポーネント:

import { getTranslation } from '@payloadcms/translations'

async function MyServerComponent({ i18n }) {
  const translatedTitle = getTranslation(myTranslation, i18n)
  return <p>{translatedTitle}</p>
}

クライアントコンポーネント:

'use client'
import { useTranslation } from '@payloadcms/ui'

export function MyClientComponent() {
  const { t, i18n } = useTranslation()

  return (
    <div>
      <p>{t('namespace:key', { variable: 'value' })}</p>
      <p>Language: {i18n.language}</p>
    </div>
  )
}

コンポーネントのスタイル設定

CSS 変数の使用
import './styles.scss'

export function MyComponent() {
  return <div className="my-component">Custom Component</div>
}
// styles.scss
.my-component {
  background-color: var(--theme-elevation-500);
  color: var(--theme-text);
  padding: var(--base);
  border-radius: var(--border-radius-m);
}
Payload SCSS のインポート
@import '~@payloadcms/ui/scss';

.my-component {
  @include mid-break {
    background-color: var(--theme-elevation-900);
  }
}

一般的なパターン

条件付きフィールド表示
'use client'
import { useFormFields } from '@payloadcms/ui'
import type { TextFieldClientComponent } from 'payload'

export const ConditionalField: TextFieldClientComponent = ({ path }) => {
  const showField = useFormFields(([fields]) => fields.enableFeature?.value)

  if (!showField) return null

  return <input type="text" />
}
API からのデータ読み込み
'use client'
import { useState, useEffect } from 'react'

export function DataLoader() {
  const [data, setData] = useState(null)

  useEffect(() => {
    fetch('/api/custom-data')
      .then((res) => res.json())
      .then(setData)
  }, [])

  return <div>{JSON.stringify(data)}</div>
}
サーバーコンポーネントでの Local API の使用
import type { Payload } from 'payload'

async function RelatedPosts({ payload, id }: { payload: Payload; id: string }) {
  const post = await payload.findByID({
    collection: 'posts',
    id,
    depth: 0,
  })

  const related = await payload.find({
    collection: 'posts',
    where: {
      category: { equals: post.category },
      id: { not_equals: id },
    },
    limit: 5,
  })

  return (
    <div>
      <h3>Related Posts</h3>
      <ul>
        {related.docs.map((doc) => (
          <li key={doc.id}>{doc.title}</li>
        ))}
      </ul>
    </div>
  )
}

export default RelatedPosts

パフォーマンスのベストプラクティス

1. クライアントバンドルサイズを最小限に抑える
// In Admin Panel UI: import from @payloadcms/ui
'use client'
import { Button } from '@payloadcms/ui'

// In frontend code: use specific paths for tree-shaking
import { Button } from '@payloadcms/ui/elements/Button'
2. 再レンダリングを最適化する
// Bad: Re-renders on every form change
'use client'
import { useForm } from '@payloadcms/ui'

export function MyComponent() {
  const { fields } = useForm()
  // Re-renders on ANY field change
}

// Good: Only re-renders when specific field changes
;('use client')
import { useFormFields } from '@payloadcms/ui'

export function MyComponent({ path }) {
  const value = useFormFields(([fields]) => fields[path])
  // Only re-renders when this field changes
}
3. 可能な場合はサーバーコンポーネントを使用する

クライアントコンポーネントは、次が必要な場合にのみ使用してください。

  • 状態(useState、useReducer)
  • エフェクト(useEffect)
  • イベントハンドラー(onClick、onChange)
  • ブラウザー API(localStorage、window)
4. React のベストプラクティス
  • 負荷の高いコンポーネントには React.memo() を使用する
  • リスト内で適切な key props を設定する
  • レンダリング内でのインライン関数定義を避ける
  • 非同期処理には Suspense 境界を使用する

インポートマップ

Payload は、すべてのコンポーネントパスを解決するインポートマップを app/(payload)/admin/importMap.js に生成します。

手動で再生成する:

payload generate:importmap

場所を上書きする:

export default buildConfig({
  admin: {
    importMap: {
      baseDir: path.resolve(dirname, 'src'),
      importMapFile: path.resolve(dirname, 'app', 'custom-import-map.js'),
    },
  },
})

型安全性

コンポーネントには Payload の TypeScript 型を使用してください。

import type {
  TextFieldServerComponent,
  TextFieldClientComponent,
  TextFieldCellComponent,
} from 'payload'

export const MyFieldComponent: TextFieldServerComponent = (props) => {
  // Fully typed props
}

トラブルシューティング

「useConfig is undefined」または類似のフックエラー

原因: Payload パッケージ間の依存バージョンが一致していません。

解決策: すべての @payloadcms/* パッケージを完全に同じバージョンに固定します。

{
  "dependencies": {
    "payload": "3.0.0",
    "@payloadcms/ui": "3.0.0",
    "@payloadcms/richtext-lexical": "3.0.0"
  }
}
コンポーネントが読み込まれない
  1. ファイルパスが正しいことを確認します(baseDir からの相対パス)
  2. 名前付きエクスポートの構文を確認します:/path/to/file#ExportName
  3. payload generate:importmap を実行して再生成します
  4. コンポーネントファイルに TypeScript エラーがないか確認します

リソース

コメント