spree-dashboard について
spree-dashboardは、Spree 6のReact管理ダッシュボードをカスタマイズする開発者向けスキルです。ダッシュボードAPIを使い、プラグイン、ルート、ナビゲーション、スロット、テーブル、フォーム、権限を拡張する方法を説明します。
主な機能
- 01ダッシュボードプラグインの登録
- 02ナビゲーションとルートのカスタマイズ
- 03スロットによるウィジェット追加
- 04テーブルとフォーム項目の拡張
- 05ダッシュボード権限APIの利用
活用シーン
- 01Spree管理ダッシュボードへの独自ページ追加
- 02組み込み管理ページへのウィジェット配置
- 03管理テーブルやフォームの拡張
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ spree/ agent-skills --skill spree-dashboardインストール後、Claude Code で/spree-dashboardを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/public-api.mdrecipes.mdslots.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-started50、home100、orders200、products300、inventory350、customers400、sellers450、loyalty475、promotions500、reports600、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')が解決されるよう、バックエンドにカタログスコープを登録します。
Ability と# server/config/initializers/spree.rb Spree.permissions.register_scope(:reports, group: :analytics, resources: -> { [MyApp::Report] })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_dashboardgem は、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/update | nav.add/remove/update、nav.addChild。設定には settingsNav |
spree:admin:scaffold と ERB CRUD ビュー | defineTable と <ResourceTable> ページ、および routes エントリー(API は spree:api_resource が提供) |
Spree.admin.tables.<t>.add/remove/update | tables.<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 コントローラー、importmap | React コンポーネントと 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/react1.8.0、recharts3.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。