mcpField
S
spree-dashboard

spree-dashboard

github.com/spree開発10104 回インストール4 週間前に更新
リポジトリ

spree-dashboard について

spree-dashboardは、Spree 6のReact管理ダッシュボードをカスタマイズする開発者向けスキルです。ダッシュボードAPIを使い、プラグイン、ルート、ナビゲーション、スロット、テーブル、フォーム、権限を拡張する方法を説明します。

主な機能

  1. 01ダッシュボードプラグインの登録
  2. 02ナビゲーションとルートのカスタマイズ
  3. 03スロットによるウィジェット追加
  4. 04テーブルとフォーム項目の拡張
  5. 05ダッシュボード権限APIの利用

活用シーン

  1. 01Spree管理ダッシュボードへの独自ページ追加
  2. 02組み込み管理ページへのウィジェット配置
  3. 03管理テーブルやフォームの拡張

インストール

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

$npx skills add https://github.com/spree/agent-skills --skill spree-dashboard

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
public-api.md
recipes.md
slots.md

詳細

Spree Dashboard(React 管理画面)— アプリ内カスタマイズ

Spree 6 の管理画面は、Admin API(/api/v3/admin/*)のみと通信する React SPA です。これは開発者プレビューであり、パッケージのバージョンは 1.0.0-beta.x で、API も変更される可能性があるため、名前が解決できない場合は、インストール済みのエクスポート一覧(node_modules/@spree/dashboard-core/src/index.ts)を確認してください。Rails の spree_admin gem はなくなっており、上書きできる ERB ビュー、パーシャル、Stimulus、Turbo はありません。

このスキルは、独自のダッシュボードアプリをカスタマイズするためのものです。他のストアがインストールできる npm パッケージとしてカスタマイズを提供する場合は、spree-dashboard-plugins を参照してください(同じ API を使用し、パッケージ化の方法が異なります)。

配置場所

my-store/
├── server/                 # Rails app (Spree gems, Admin API) — `backend/` in older projects
└── apps/
    ├── dashboard/          # the "host app" — you own this
    │   ├── src/main.tsx        # mounts <Dashboard>, imports plugins + virtual:spree-dashboard-plugins
    │   ├── src/plugins.ts      # ← your registrations
    │   ├── src/styles.css      # @import "@spree/dashboard/styles.css" + your token overrides
    │   ├── src/routeTree.gen.ts  # generated on every dev start/build (and by `spree add`) — commit it
    │   └── vite.config.ts      # spreeDashboardPlugin() + /api proxy
    └── seller-dashboard/   # optional marketplace seller panel (same model)
  • 新規プロジェクト:npx create-spree-app my-store を実行し、React Dashboard に「Yes」と回答します。既存プロジェクト:spree add dashboard(--template <git-url|path>、--no-install)。
  • 開発:spree dev で API を起動し、cd apps/dashboard && pnpm dev を実行します → http://localhost:5173。管理者のメールアドレスとパスワードでサインインします。API キーは不要です(JWT はメモリ内に保持され、リフレッシュ Cookie は httpOnly です)。Vite は /api と /rails を VITE_API_PROXY_TARGET(デフォルトは http://localhost:3000)にプロキシします。
  • すべてのページはストアのスコープ内にあります。URL は /$storeId/... です。登録するパスにはストアのプレフィックスを付けません。

技術スタック:Vite、React 19、TanStack Router(ファイルベース)+ TanStack Query v5、React Hook Form + Zod、Tailwind v4 + shadcn/ui + Base UI、lucide-react アイコン、i18next、Biome、@spree/admin-sdk。

パッケージ — @spree/dashboard からインポート
パッケージ内容
@spree/dashboard-uiデザインシステム:shadcn プリミティブ、ResourceLayout、Empty*、RelativeTime、useConfirm、トークン。props を渡すだけで、プロバイダーは不要です。
@spree/dashboard-coreフレームワーク:レジストリ、defineDashboardPlugin、プロバイダー/フック(useAuth、usePermissions、useStore)、adminClient、ResourceTable、PageHeader、Slot。
@spree/dashboardアプリシェル(ルート、リソースページ、ロケール)と、上記 2 パッケージを再エクスポートするファサード。

ホストアプリでは、常に import { … } from '@spree/dashboard' を使用してください。配布するプラグインのみが -core/-ui を直接インポートできます。その場合も、各パッケージのエントリーポイントからのみインポートし、ファイルパスを深く指定しないでください。パッケージはソースを同梱しており、深いパスからインポートすると、インストール先アプリで Vite が CommonJS 依存関係を変換できないまま残すため、ダッシュボードはレンダリング前に停止します(1.0.0-beta.4 のセットアップ画面でまさにこの問題が発生しました)。完全な一覧は references/public-api.md を参照してください。

コードの配置場所(スターターの規約)
パス内容
src/plugins.ts登録のみ — 追加した項目のマップ
src/pages/1 ファイルにつき 1 画面
src/hooks/Admin API のラッパー(use-brands.ts)— コンポーネントから adminClient を直接呼び出さないでください
src/tables/defineTable の呼び出し
src/schemas/Zod スキーマとフォーム値の型
src/slots/組み込みページに挿入するウィジェット
src/locales/独自の admin.* 翻訳バンドル

defineDashboardPlugin — 唯一のエントリーポイント

// src/plugins.ts
import { defineDashboardPlugin, i18n } from '@spree/dashboard'
import { BarChartIcon } from 'lucide-react'
import en from './locales/en.json'
import { AnalyticsPage } from './pages/analytics'

i18n.addResourceBundle('en', 'translation', en, true, true) // translations first

defineDashboardPlugin({
  nav: [{ key: 'analytics', labelKey: 'admin.analytics.nav', path: '/analytics', icon: BarChartIcon, position: 650 }],
  routes: [{ key: 'analytics', path: '/analytics', component: AnalyticsPage }],
})
キー説明
locales{ en: bundle, de: bundle } — 呼び出し内で、ほかのすべてに先立って登録されます
nav配列(追加)または { add, remove, update, addChildren }
settingsNavGroups / settingsNav設定サブシェルのグループ、続いてエントリー(配列または { add, remove, update })
routes/$storeId/… 配下に実行時登録されるページ
slots{ 'slot.name': [{ id, component, position?, if? }] }
tables{ products: { add, update, remove } } による列の変更
formFields{ product: [{ name, from }] } 組み込みフォームの拡張フィールド
customFieldComponents{ 'namespace.key': Component } カスタムフィールド定義用のカスタムウィジェット

レジストリはモジュールのシングルトンです。キー/IDは一意である必要があります(組み込みのものとの重複も不可)。重複すると起動時に例外が発生します。ファサードはすべてのエラーを収集し、最後に一度だけ再スローします(AggregateError)。各レジストリには命令型 API もあります(nav.add、registerSlot、tables.products.addColumn など)。細かく制御したい場合や、型安全なスロットを使いたい場合に利用してください。

ナビゲーション

import { nav, settingsNav } from '@spree/dashboard'

nav.addChild('products', { key: 'products.brands', labelKey: 'admin.brands.nav', path: '/brands', subject: 'Spree::Brand' })
nav.insertAfter('orders', { key: 'returns-hub', label: 'Returns hub', path: '/returns-hub' })
nav.update('orders', { position: 150 })
nav.remove('loyalty')
nav.add({ key: 'help', label: 'Help', path: '/help', section: 'bottom' })       // pinned to the footer
nav.add({ key: 'onboarding', label: 'Onboarding', path: '/onboarding',
  if: ({ store }) => !isConfigured(store), badge: PendingCountBadge })        // badge is a component
  • NavEntry:key、label | labelKey、path(必須、先頭に /)、icon、section('main'|'bottom')、position、subject + action(デフォルトは 'read')、if、badge、children。
  • 組み込みのトップレベルキー/位置:getting-started 50、home 100、orders 200、products 300、inventory 350、customers 400、sellers 450、loyalty 475、promotions 500、reports 600、settings(bottom)。末尾に追加する場合は 650 以上を使用してください。
  • update(parent, { children }) は子項目を置き換えます。組み込みメニューの下にネストするには、addChild / addChildren を使用してください。
  • label: i18n.t(...) よりも labelKey を優先してください。リテラルのラベルは登録時のまま固定され、言語を切り替えても変わりません。
  • 設定グループ:store、selling、shipping、marketplace、team、developer。設定パスには /$storeId/settings がプレフィックスとして付加されます。新しいグループの場合:settingsNavGroups: [{ key: 'integrations', labelKey: '…', position: 700 }]、続いて settingsNav: [{ key: 'erp', label: 'ERP', path: '/erp', group: 'integrations', subject: 'Spree::Store', action: 'update' }](comingSoon: true を指定すると、無効化された「近日公開」エントリーが表示されます)。

ルート(カスタムページ)

routes: [
  { key: 'reports', path: '/reports', component: ReportsPage, subject: 'Spree::Order' },
  { key: 'report', path: '/reports/$reportId', component: ReportPage },
]

function ReportPage({ params, storeId, searchParams }: { params: Record<string, string>; storeId: string; searchParams: Record<string, unknown> }) {
  return <ResourceLayout header={<PageHeader title="Report" />} main={<ReportBody id={params.reportId} />} />
}
  • レジストリのルートは、ナビゲーション時にキャッチオール(/_authenticated/$storeId/$)で照合され、認証ガードと共通 UI を継承し、後から登録することもできます。subject を指定すると、直接移動した場合に 403 ページが表示されます。ナビゲーションエントリーにも同じ subject を指定してください。
  • 型付きルートツリーには含まれないため、リンクにはキャストが必要です:<Link to={'/$storeId/reports/$reportId' as string} params={{ storeId, reportId } as never}>。
  • 同じ URL にコンパイル済みのファイルルートがある場合、そちらが暗黙的に優先されます。ファイルルート(型付き、コード分割、validateSearch)はパッケージ化されたプラグインの仕組みです。spree-dashboard-plugins を参照してください。
  • ページの状態(フィルター、並べ替え、ページ、開いているシート ?edit=prod_…)は React の状態ではなく、URL に保持してください。

スロット — 組み込みページ内のウィジェット

defineDashboardPlugin({
  slots: { 'product.form_sidebar': [{ id: 'acme-brand-card', component: BrandCard as never, position: 50 }] },
})
// or fully typed:
registerSlot<{ product: Product }>('product.form_sidebar', { id: 'acme-brand-card', component: ({ product }) => <BrandCard product={product} /> })
  • コンポーネントに渡されるのはスロットのコンテキストのみです({ product }、{ order }、{ resource } など)。permissions/store/user は暗黙的に注入されません。usePermissions()、useStore()、useAuth() を呼び出してください。エントリーの if が参照するのもスロットコンテキストのみです。
  • ファサードでは as never が想定されています(スロットマップの型は消去されます)。型安全性が必要な場合は registerSlot<Ctx> を使用してください。removeSlot(name, id) / updateSlot(name, id, patch) を使うと、別のプラグインのエントリーも含め、任意のエントリーを変更できます。
  • 主なスロット:page.actions、page.actions_dropdown、page.tabs、product|category|collection|order|customer.form_sidebar、store.form_main、company.form_main|form_sidebar、seller.form_main|form_sidebar、およびタイプごとのエディタースロット(payment_method.form.<type>、promotion.rule_form.<type> など)。コンテキストを含む全カタログ:references/slots.md。

テーブル

import { tables } from '@spree/dashboard'

tables.products.addColumn({ key: 'brand', label: 'Brand', default: true, render: (p) => p.brand?.name ?? '—' })
tables.products.updateColumn('price', { label: 'Retail price' })
tables.products.removeColumn('tags')
tables.orders.addColumn({ key: 'channel_id', label: 'Channel', displayable: false, filterable: true, filterType: 'enum',
  filterOptions: [{ value: 'ch_web', label: 'Web' }] })            // filter-only entry
  • ColumnDef:key、label、render、className、default(デフォルトで表示)、sortable、filterable、filterType(デフォルトは string | boolean | number | currency | date | enum + filterOptions | resource + filterResource { queryKey, search, hydrate, getOptionLabel } | tags + taggableType)、ransackAttribute、displayable、expand、quickFilter。
  • 並べ替えとフィルタリングは Ransack を介してサーバー側で実行されます。属性がモデルの whitelisted_ransackable_attributes に含まれていない場合、リクエストは失敗します。
  • まだ登録されていないテーブルへの変更は(組み込みテーブルはページの初回読み込み時に登録されます)、キューに追加されて再実行されます。オプション機能でも安全に使用できます。組み込みテーブルのキーには、products、orders、customers、promotions、collections、stock-levels、gift-cards、store-credits、sellers、companies、returns、price-lists などがあります。
  • 独自の一覧ページ:defineTable<T>('brands', { columns, searchParam, defaultSort, emptyMessage }) + <ResourceTable tableKey queryKey queryFn searchParams />(ページネーション、フィルターパネル、列選択、URL との双方向変換に対応)。bulkActions / rowActions / reorder は、独自の <ResourceTable> に渡すプロパティです。組み込みテーブルに追加することはできません。

フォームフィールドとカスタムフィールドのウィジェット

  • カスタムフィールドの定義(コード不要)は、すでにカスタムフィールドカードに表示され、フォームとともに保存されます。定義のウィジェットを置き換えるには、customFieldComponents: { 'specs.color': ColorInput } を指定します(プロパティ:id、ariaLabel、value、onChange、definition)。
  • 組み込みフォームの実際の列:formFields: { product: [{ name: 'lead_time_days', from: (p) => p?.lead_time_days ?? null }] } と、useHostForm()(form.register(...) または <Controller control={form.control}>)で入力フィールドをバインドするスロットウィジェットを使用します。値が初期化され、保存が有効になり、ページの PATCH リクエストに含まれ、サーバーの 422 エラーがインラインで表示されます。ホストフォーム:product、category、collection、store。入れ子の <form> は絶対にレンダリングしないでください。注文/顧客ページでは useOptionalHostForm() を使用し、自分で保存してください。

両方の方法に加え、ページアクション、サイドバーウィジェット、カスタム一覧ページの実用的なコード例については、references/recipes.md を参照してください。

権限 — UI による制御は認可ではありません

import { Can, usePermissions } from '@spree/dashboard'

const { permissions, permissionKeys, rules, isLoading, refresh } = usePermissions()
permissions.can('update', 'Spree::Order')            // class-level, mirrors server abilities
permissions.isConditional('update', 'Spree::Order')  // true → record-level rules apply; expect a possible 403
permissionKeys.includes('write_orders')              // flat catalog keys the role holds on this store

<Can I="destroy" a="Spree::Product" fallback={null}><DeleteButton /></Can>
  • ナビゲーション項目、設定項目、ルートでは subject(および action)を指定します。ナビゲーション項目では if({ permissions, store, user })も指定でき、スロットウィジェットはコンポーネント内で制御します。
  • ボタンを非表示にするのは UX 上の対応です。Admin API はすべてのリクエストで、コントローラーごとの read_*/write_* 権限を強制します。独自のエンドポイントでも同様にしてください(spree-auth-permissions を参照)。
  • 新しいリソースの場合、ロールに権限を付与でき、permissions.can('read', 'MyApp::Report') が解決されるよう、バックエンドにカタログスコープを登録します。
    # server/config/initializers/spree.rb
    Spree.permissions.register_scope(:reports, group: :analytics, resources: -> { [MyApp::Report] })
    
    Ability と permission_keys は GET /api/v3/admin/me とともに提供されます。ストアの切り替え時に権限が再読み込みされます。

バックエンドとのやり取り

// src/hooks/use-brands.ts
import { adminClient, useResourceKey, useResourceMutation } from '@spree/dashboard'
import type { PaginatedResponse } from '@spree/admin-sdk'
import { useQuery } from '@tanstack/react-query'

export function useBrands(params?: Record<string, unknown>) {
  return useQuery({
    queryKey: useResourceKey('brands', params),   // → ['brands', storeId, params]
    queryFn: () => adminClient.request<PaginatedResponse<Brand>>('GET', '/brands', { params }),
  })
}

export function useUpdateBrand(id: string) {
  return useResourceMutation({
    mutationFn: (body: Partial<Brand>) => adminClient.request<Brand>('PATCH', `/brands/${id}`, { body }),
    invalidate: [['brands'], ['brands', id]],      // store id injected for you
    successMessage: 'Saved',
  })
}
  • adminClient は設定済みの @spree/admin-sdk シングルトンです。型付きリソース(adminClient.products.list/get/update…)と、/api/v3/admin を基準とするパスを指定する request<T>(method, path, { params, body }) を利用します。クライアントを追加で作成したり、生の fetch を使ったりしないでください。
  • すべてのクエリーキーをストア単位にしてください。フックでは useResourceKey、ID が後から渡される場合は useResourceKeyBuilder()、React の外部では withStoreScope(key, storeId) を使います。単独の ['brands'] を使うと、ストアを切り替えた後に、あるストアの商品が別のストアに表示されます。
  • エラーは SpreeError(status、code、details)です。useResourceMutation は 422 以外のエラーではトーストを表示し、422 ではフォームでエラーを表示できるよう何も表示しません。catch (e) { if (!mapSpreeErrorsToForm(e, form.setError)) throw e }(:base → errors.root)。フォームのないページ(ヘッダーのアクション、ダイアログのないボタン)では、showValidationErrors: true を渡してください。そうしないと、サーバーが拒否した場合(理由を含む 422 エラー)に、マーチャントには何も表示されません。
  • リソースをまたぐフィルタリング、ソート、集計は、クライアントから複数回リクエストするのではなく、バックエンドのエンドポイントで行います。
  • ID はどこでもプレフィックス付き文字列(prod_…、or_…、cust_…)です。解析したり、プレフィックスを削除したりしないでください。

エンドツーエンド:新しい商品属性(フォーム+列+フィルター+ソート)

Ransack のフィルター/ソートや DB による保証が必要な場合を除き、カスタムフィールド定義を優先してください。実際の列を追加する場合:

# 1. server/db/migrate/…_add_lead_time_days_to_spree_products.rb
class AddLeadTimeDaysToSpreeProducts < ActiveRecord::Migration[8.1]
  def change
    add_column :spree_products, :lead_time_days, :integer
    add_index :spree_products, :lead_time_days
  end
end

# 2. server/config/initializers/spree.rb — writable, filterable/sortable
Spree::Product.additional_permitted_attributes += [:lead_time_days]   # += (never << — it's frozen; never = — drops other extensions')
Spree::Product.whitelisted_ransackable_attributes |= %w[lead_time_days]
Spree.api.admin_product_serializer = 'MyApp::AdminProductSerializer'

# 3. server/app/serializers/my_app/admin_product_serializer.rb — readable
module MyApp
  class AdminProductSerializer < Spree::Api::V3::Admin::ProductSerializer
    typelize lead_time_days: [:number, nullable: true]
    attributes :lead_time_days
  end
end

# + a model validation — the dashboard renders its 422 message inline
// 4. apps/dashboard/src/plugins.ts
defineDashboardPlugin({
  formFields: { product: [{ name: 'lead_time_days', from: (p) => p?.lead_time_days ?? null }] },
  slots: { 'product.form_sidebar': [{ id: 'lead-time', component: LeadTimeCard as never, position: 60 }] },
  tables: { products: { add: [{
    key: 'lead_time_days', label: i18n.t('admin.fields.product.lead_time_days.label'),
    sortable: true, filterable: true, filterType: 'number', default: true,
    render: (p) => (p.lead_time_days != null ? `${p.lead_time_days} d` : '—'),
  }] } },
})

LeadTimeCard は useHostForm<{ lead_time_days: number | null }>() と form.register('lead_time_days', { setValueAs: (v) => (v === '' || v == null ? null : Number(v)) }) を使います。フォームの name、シリアライザーの属性、許可された属性は、すべて同じ文字列にする必要があります。まずバックエンドを確認します。spree api patch products/prod_… --data '{"lead_time_days":14}' と spree api get "products?q[lead_time_days_gt]=7&sort=-lead_time_days" を実行してください。カスタム Admin コントローラーでは、代わりに resource_permitted_attributes を使います(spree-resource、spree-decorators を参照)。

テーマ設定

すべて CSS カスタムプロパティで設定します。src/styles.css のインポートの後にオーバーライドを記述し、:root と .dark の両方に設定してください(テーマプロバイダーは .dark クラスのみを切り替えます)。

@import "@spree/dashboard/styles.css";

:root { --primary: oklch(0.55 0.22 264); --radius: 0.25rem; }
.dark { --primary: oklch(0.72 0.19 264); }

@theme inline { --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif; --text-base: 0.9375rem; }

トークン:--background/--foreground、--card*、--popover*、--muted*、--primary*(デフォルトでは --foreground のエイリアス)、--secondary*、--accent*(-hover、-strong を含む)、--link、--ring、--destructive*、--status-{green,amber,red,blue}-{bg,border,fg}、--border*、--input、--radius、--sidebar*、--chart-1..5。ページでは生の var(--…) ではなく、Tailwind ユーティリティ(bg-card text-muted-foreground)を使い、本文のリンクには className="link" を使います。販売者パネル(@spree/seller-dashboard/styles.css)も同じトークンを使用します。

翻訳

i18next を使用し、名前空間は translation のみ、すべてのキーを admin. 配下に置きます。plugins.ts の先頭で i18n.addResourceBundle(locale, 'translation', { admin: { … } }, true, true)(または defineDashboardPlugin({ locales: { en, de } }))を登録し、コンポーネントでは useTranslation()、レジストリーでは labelKey を使います。フィールドキーは admin.fields.<resource>.<attribute>.label|placeholder で、該当するキーがない場合は admin.fields.<attribute>.* にフォールバックします。組み込みロケール:en、de、es、fr、pl、ar、zh-CN。プロモーションルール/アクション、計算機、価格ルール、コレクションルール、注文ルーティングルール、配送方法ルール、コミッションルール、販売者要件、インテグレーション、または権限を登録する gem は、admin.types.<family>.<code>.{name,description} も提供してください。提供しない場合は、API のラベル(管理者のロケールではなく、ストアのロケールで解決されたもの)が表示されます。サーバーの 422 メッセージはそのまま表示されるため、Rails i18n でローカライズしてください。

テスト

cd apps/dashboard && pnpm test(Vitest、Node 環境、src/**/*.test.ts)および pnpm test:e2e(spree dev と開発サーバーに対する Playwright。デプロイ先を指定するには E2E_BASE_URL を使用します)。

  • UI と API の間のロジックをユニットテストします。クエリキー、ペイロードマッパー、権限判定、フィルターから Ransack へのマッピングが対象です。マークアップを検証するためにコンポーネントをレンダリングしないでください。また、1 行だけの adminClient.request ラッパーもテストしないでください。
    import { withStoreScope } from '@spree/dashboard'
    import { QueryClient } from '@tanstack/react-query'
    it('invalidates only the current store', () => {
      const qc = new QueryClient()
      qc.setQueryData(withStoreScope(['brands'], 'store_a'), [])
      qc.setQueryData(withStoreScope(['brands'], 'store_b'), [])
      qc.invalidateQueries({ queryKey: withStoreScope(['brands'], 'store_a') })
      expect(qc.getQueryCache().getAll().filter((q) => q.state.isInvalidated)).toHaveLength(1)
    })
    
  • E2E:UI を操作して検証します(goto('/brands') ではなくサイドバーのリンクをクリックします。実際のパスは /:storeId/… です)。テストは同じ DB を共有し、設定ではワーカーを 1 つ実行するため、レコード名の末尾には Date.now() を付けます。
  • プッシュ前のチェック:pnpm typecheck && pnpm lint && pnpm test && pnpm build。

デプロイ(概要 — spree-deployment を参照)

  • シングルノード(デフォルト): spree_dashboard gem は、Spree::Dashboard.dist_path / SPREE_DASHBOARD_DIST_PATH からビルド済みのダッシュボードを /dashboard(セラーパネルは /sellers)で提供します。公式イメージには標準ビルドが含まれています。カスタマイズしたビルドは spree build --production [--tag …](または docker build . -f server/Dockerfile)で作成してください。apps/dashboard/ を検出し、VITE_BASE_PATH=/dashboard/ と相対 API URL を使ってビルドします。
  • 静的ホスト / CDN: VITE_SPREE_API_URL=https://api.example.com pnpm build を実行し、/* → /index.html のリライトを設定して dist/ をデプロイします。両側で HTTPS を有効にし(リフレッシュ Cookie は SameSite=None; Secure です)、オリジンを 設定 → 開発者 → 許可されたオリジン に追加します。環境ごとにビルドしてください。
  • VITE_* の値はバンドルにコンパイルされます。ここにシークレットを含めないでください。

spree_admin(Spree 5.x)からの移行

spree_adminダッシュボード
Spree.admin.navigation.sidebar.add/remove/updatenav.add/remove/update、nav.addChild。設定には settingsNav
spree:admin:scaffold と ERB CRUD ビューdefineTable と <ResourceTable> ページ、および routes エントリー(API は spree:api_resource が提供)
Spree.admin.tables.<t>.add/remove/updatetables.<t>.addColumn/removeColumn/updateColumn
Spree.admin.partials.<point> << 'partial'slots: { '<slot>': [...] }
_form.html.erb / product_form partial へのフィールド追加formFields と useHostForm() を使うスロットウィジェット、またはカスタムフィールド定義
管理コントローラーのデコレーター / permitted_attributes管理 API:Model.additional_permitted_attributes +=、シリアライザーは Spree.api.admin_*_serializer
ビュー内の CanCanCan can?usePermissions() / <Can> / subject(UX のみ)とバックエンドの権限キー
Stimulus コントローラー、importmapReact コンポーネントと TanStack Query フック
管理画面の Tailwind / CSS のオーバーライドsrc/styles.css 内の CSS トークンのオーバーライド
Spree.t の管理画面文字列i18next の admin.* バンドル

5.x から 6 への移行の詳細は、spree-upgrade-5-to-6 を参照してください。

注意点

  • 重複するキー/IDがあると起動時にエラーになります — 名前空間キー(acme-reports)を使い、組み込みキー(orders、products、…)との重複にも注意してください。
  • スコープなしのクエリキーを使うと、ストア間でデータが漏えいします。 常に useResourceKey を使ってください。
  • スロットウィジェット側で表示を制御する必要があります — page.actions はすべてのページに表示されます。権限は props として渡されません。
  • nav.update(parent, { children }) を使うと組み込みの子要素が削除されます — addChild を使ってください。
  • Ransack の許可リストにエントリーがない並べ替え/フィルタ可能な列は、ビルド時ではなくリクエスト時に失敗します。
  • 同じ URL にファイルルートがあると、レジストリルートがそちらに隠れて表示されません — エラーも発生しません。
  • ラベルは固定されます: label: i18n.t(...) は言語を切り替えても更新されません。labelKey を使ってください。
  • ホストフォームスロット内にネストした <form> を置くと、ページの保存が正しく動作しなくなります。 useHostForm() を使ってください。
  • フォーム値の型に SDK のエンティティ型を直接含めないでください — RHF がネストしたキーをたどるため、TS コンパイラでエラーが発生します。フラットな値の型にマッピングしてください。
  • ダッシュボードの依存関係の固定バージョンを緩めないでください。 @spree/dashboard-ui はアプリにコンパイルされるため、その依存関係は Spree でテスト済みのバージョン(@base-ui/react 1.8.0、recharts 3.10.1、…)に正確に固定されています(override、キャレット範囲、または 1.0.0-beta.7 より古いダッシュボードパッケージ経由で)新しい Base UI が混入すると、最初のレンダリング前に useSyncExternalStore に関する SyntaxError が発生します。Base UI を独自に更新するのではなく、@spree/dashboard* パッケージをまとめてアップグレードしてください。
  • Rails に直接アクセスしないでください。 エンドポイントが不足している場合は、まず Admin API に追加します(spree-resource、spree-api-v3)。

さらに読む

  • ドキュメント(インストール済み):node_modules/@spree/docs/dist/developer/dashboard/ — overview.md、concepts.md、customization/{quickstart,navigation,routes,slots,tables,permissions,backend,theming,translations}.md、recipes/*.md、slots-catalog.md、public-api.md、deployment.md。チュートリアル:developer/tutorial/dashboard.md および developer/tutorial/testing.md。オンライン:https://spreecommerce.org/docs/developer/dashboard/overview
  • アプリ内の apps/dashboard/AGENTS.md(規約)と README.md。
  • ソース:packages/dashboard-core/src/{plugin.ts,lib/*-registry.ts}、packages/dashboard/src/nav/default.ts(組み込みナビゲーション)、https://github.com/spree/spree の packages/dashboard-plugin-example(Brands の完全な例)
  • 関連スキル:spree-dashboard-plugins、spree-auth-permissions、spree-api-v3、spree-typescript-sdk、spree-resource、spree-catalog(カスタムフィールド)、spree-deployment。

コメント