pimcore-studio
Build Pimcore Studio v2 features - React/TypeScript, Ant Design, plugins, modules, dynamic types, DI container, widgets
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Build Pimcore Studio v2 features - React/TypeScript, Ant Design, plugins, modules, dynamic types, DI container, widgets
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Set up Contributor License Agreement (CLA) for CORS open source repositories on GitHub
Set up or migrate a Pimcore project/bundle to use the cors/dev Docker Compose development environment
Upgrade Pimcore 11 projects and bundles to Pimcore 12 - composer, config, PHP 8.3, Symfony 7, Docker, CI/CD, cors/dev, cors/saml
Extend and customize CoreShop eCommerce - custom rules, payment gateways, entity extensions, workflows, Studio v2 plugins, notifications
Pimcore platform development - bundles, data objects, class definitions, CoreExtensions, events, workflows, documents, assets
Migrate any Pimcore ExtJS admin UI to Studio v2 React/TypeScript - panels, grids, forms, trees, stores, plugins
| name | pimcore-studio |
| description | Build Pimcore Studio v2 features - React/TypeScript, Ant Design, plugins, modules, dynamic types, DI container, widgets |
| allowed-tools | Read, Grep, Glob, Bash, Edit, Write, Task |
You are helping build features for Pimcore Studio v2 (React/TypeScript). Before writing code, load context about the architecture and patterns.
Pimcore Studio v2 is built with:
useTranslation() hook)When unsure how to do something, read the Studio source code:
vendor/pimcore/studio-ui-bundle/assets/js/src/
├── core/
│ ├── app/
│ │ ├── depency-injection/ # DI container setup (InversifyJS)
│ │ ├── module-system/ # Module registration system
│ │ ├── plugin-system/ # Plugin lifecycle (IAbstractPlugin)
│ │ ├── config/services/ # Service IDs (serviceIds constant)
│ │ ├── i18n/ # Translation setup (react-i18next)
│ │ ├── api/ # API utilities
│ │ └── public-api/ # Public API gateway, element API
│ ├── components/ # Pimcore UI components (built on antd)
│ │ ├── form/ # Form controls
│ │ ├── sidebar/ # Sidebar components
│ │ ├── split/ # Split layouts
│ │ └── ...
│ └── utils/ # Utility hooks and helpers
│ ├── hooks/ # useDebounce, useClickOutside, etc.
│ └── ...
└── modules/ # Pimcore feature modules
├── element/ # Element handling, DynamicType registries
├── widget-manager/ # Widget registry
└── ...
Search for examples:
grep -r "registerWidget\|registerDynamicType\|IAbstractPlugin" \
vendor/pimcore/studio-ui-bundle/assets/js/src/
import { type IAbstractPlugin, container } from '@pimcore/studio-ui-bundle'
const plugin: IAbstractPlugin = {
name: 'my-plugin',
onInit() {
// Phase 1: Runs first, before any modules
// - Container bindings (InversifyJS)
// - Dynamic type registration
// - Registry creation
},
onStartup({ moduleSystem }) {
// Phase 2: Runs after ALL plugins' onInit()
// - Module registration
// - Widget registration
// - Can safely use container bindings from other plugins
}
}
export default plugin
Order matters: All onInit() calls complete before any onStartup(). Bindings must happen in onInit().
Modules encapsulate feature registrations:
import { type AbstractModule, container } from '@pimcore/studio-ui-bundle'
export const MyModule: AbstractModule = {
onInit(): void {
// Register services, form builders, extensions
}
}
// In plugin's onStartup():
moduleSystem.registerModule(MyModule)
import { container } from '@pimcore/studio-ui-bundle'
// Bind
container.bind('MyServiceId').to(MyClass).inSingletonScope()
container.bind('MyServiceId').toConstantValue(myInstance)
// Retrieve
const service = container.get<MyType>('MyServiceId')
import { serviceIds } from '@pimcore/studio-ui-bundle/app'
import { type WidgetRegistry } from '@pimcore/studio-ui-bundle/modules/widget-manager'
const widgetRegistry = container.get<WidgetRegistry>(serviceIds['WidgetRegistry'])
widgetRegistry.registerWidget({ name: 'my.widget', component: MyComponent })
import { DynamicTypeObjectDataRegistry } from '@pimcore/studio-ui-bundle/modules/element'
const registry = container.get<DynamicTypeObjectDataRegistry>(
serviceIds['DynamicTypes/ObjectDataRegistry']
)
registry.registerDynamicType(new MyDynamicType())
For custom field types in the class definition editor:
import {
DynamicTypeObjectDataAbstractSelect,
DynamicTypeObjectDataAbstractMultiSelect,
DynamicTypeObjectDataAbstract,
DynamicTypeFieldFilterMultiselect,
} from '@pimcore/studio-ui-bundle/modules/element'
// Single select
export class DynamicTypeMyField extends DynamicTypeObjectDataAbstractSelect {
readonly id = 'myFieldType' // MUST match PHP CoreExtension $fieldtype
readonly dynamicTypeFieldFilterType = new DynamicTypeFieldFilterMultiselect()
}
// Multi-select
export class DynamicTypeMyFieldMulti extends DynamicTypeObjectDataAbstractMultiSelect {
readonly id = 'myFieldTypeMultiselect'
readonly dynamicTypeFieldFilterType = new DynamicTypeFieldFilterMultiselect()
}
// Custom rendering
export class DynamicTypeMyCustom extends DynamicTypeObjectDataAbstract {
readonly id = 'myCustomFieldType'
getObjectDataComponent(props: any): React.ReactElement {
return <MyCustomEditor {...props} />
}
}
Register in onInit() — never in onStartup().
import React from 'react'
import { Form, Input, Switch, InputNumber, Select } from 'antd'
import { useTranslation } from 'react-i18next'
interface MyFormProps {
data: MyEntity
onChange: (data: MyEntity) => void
}
export const MyForm: React.FC<MyFormProps> = ({ data, onChange }) => {
const { t } = useTranslation()
return (
<Form layout="vertical">
<Form.Item label={t('name')} required>
<Input value={data.name}
onChange={(e) => onChange({ ...data, name: e.target.value })} />
</Form.Item>
<Form.Item label={t('active')}>
<Switch checked={data.active}
onChange={(checked) => onChange({ ...data, active: checked })} />
</Form.Item>
</Form>
)
}
import React, { useEffect, useState } from 'react'
import { Table, Button, Space, Popconfirm } from 'antd'
export const MyList: React.FC = () => {
const [data, setData] = useState<MyEntity[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
void (async () => {
try { setData(await myApi.list()) }
finally { setLoading(false) }
})()
}, [])
return (
<Table dataSource={data} loading={loading} rowKey="id" columns={[
{ title: 'Name', dataIndex: 'name' },
{ title: 'Active', dataIndex: 'active', render: (v: boolean) => v ? 'Yes' : 'No' },
{ title: 'Actions', render: (_, record) => (
<Popconfirm title="Delete?" onConfirm={() => handleDelete(record.id)}>
<Button danger size="small">Delete</Button>
</Popconfirm>
)},
]} />
)
}
import { Modal, Form, Input } from 'antd'
const [open, setOpen] = useState(false)
<Modal title="Add Entity" open={open} onOk={handleOk} onCancel={() => setOpen(false)}>
<Form layout="vertical">
<Form.Item label="Name"><Input /></Form.Item>
</Form>
</Modal>
ALWAYS use this pattern for any Select that loads options from an API:
import React from 'react'
import { Select, type SelectProps } from 'antd'
let cachedOptions: Array<{ value: number; label: string }> | null = null
let loadPromise: Promise<typeof cachedOptions> | null = null
const loadOptions = async () => {
if (cachedOptions) return cachedOptions
if (loadPromise) return loadPromise
loadPromise = (async () => {
try {
const items = await myApi.list()
cachedOptions = items.map(i => ({ value: i.id, label: i.name }))
return cachedOptions
} finally { loadPromise = null }
})()
return loadPromise
}
export const clearCache = () => { cachedOptions = null; loadPromise = null }
export const MySelect: React.FC<SelectProps> = (props) => {
const [options, setOptions] = React.useState(cachedOptions || [])
const [loading, setLoading] = React.useState(!cachedOptions)
React.useEffect(() => {
void (async () => {
if (!cachedOptions) setLoading(true)
try { setOptions(await loadOptions()) }
finally { setLoading(false) }
})()
}, [])
return <Select {...props} loading={loading} options={options} showSearch
filterOption={(input, opt) =>
(opt?.label ?? '').toLowerCase().includes(input.toLowerCase())
} />
}
export interface MyEntity {
id: number
name: string
active: boolean
}
export const myApi = {
async list(): Promise<MyEntity[]> {
const res = await fetch('/pimcore-studio/api/my-bundle/entities')
return (await res.json()).data ?? (await res.json())
},
async get(id: number): Promise<MyEntity> {
return (await fetch(`/pimcore-studio/api/my-bundle/entities/${id}`)).json()
},
async save(entity: Partial<MyEntity>): Promise<MyEntity> {
const method = entity.id ? 'PUT' : 'POST'
const url = entity.id
? `/pimcore-studio/api/my-bundle/entities/${entity.id}`
: '/pimcore-studio/api/my-bundle/entities'
return (await fetch(url, {
method, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(entity)
})).json()
},
async delete(id: number): Promise<void> {
await fetch(`/pimcore-studio/api/my-bundle/entities/${id}`, { method: 'DELETE' })
},
}
File: Resources/translations/studio.en.yaml
my_entity: 'My Entity'
my_entity_name: 'Name'
Usage:
import { useTranslation } from 'react-i18next'
const { t } = useTranslation()
t('my_entity_name')
npm run build — Production buildnpm run dev — Development with hot reload{
"name": "@my-scope/my-plugin",
"version": "1.0.0",
"main": "src/index.ts",
"dependencies": {
"@pimcore/studio-ui-bundle": "^1.0.0",
"antd": "^5.12.0",
"react": "18.3.x",
"react-i18next": "^14.0.0"
}
}
Resources/assets/pimcore-studio/
├── package.json
└── src/
├── main.ts # Plugin entry (IAbstractPlugin)
├── modules/
│ ├── icon-library/ # Icon registrations
│ └── {feature}/
│ ├── api.ts # API layer
│ ├── MyComponent.tsx # React components (using antd)
│ └── index.ts
├── components/ # Shared components
│ └── MySelect.tsx # Selects with caching
└── dynamic-types/ # Pimcore field types
└── DynamicTypeMyField.tsx
See .claude/skills/pimcore-studio/patterns.md for complete code templates.