# Pimcore Studio

> Build Pimcore Studio v2 features - React/TypeScript, Ant Design, plugins, modules, dynamic types, DI container, widgets

- **Type:** Skill
- **Install:** `agentstack add skill-cors-gmbh-pimcore-skills-pimcore-studio`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [cors-gmbh](https://agentstack.voostack.com/s/cors-gmbh)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [cors-gmbh](https://github.com/cors-gmbh)
- **Source:** https://github.com/cors-gmbh/pimcore-skills/tree/main/skills/pimcore-studio

## Install

```sh
agentstack add skill-cors-gmbh-pimcore-skills-pimcore-studio
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Pimcore Studio v2 Development

You are helping build features for Pimcore Studio v2 (React/TypeScript). Before writing code, load context about the architecture and patterns.

## Tech Stack

Pimcore Studio v2 is built with:
- **React 18** — UI components
- **TypeScript** — Type safety
- **Ant Design (antd)** — UI component library (Form, Table, Modal, Select, Input, Button, Tabs, Tree, etc.)
- **InversifyJS** — Dependency injection container
- **react-i18next** — Internationalization (`useTranslation()` hook)
- **Rsbuild** — Build tool with Module Federation for plugin loading

## Discovering Pimcore Studio Source

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:
```bash
grep -r "registerWidget\|registerDynamicType\|IAbstractPlugin" \
  vendor/pimcore/studio-ui-bundle/assets/js/src/
```

## Step 1: Plugin Architecture

### Plugin Lifecycle

```typescript
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()`.

### Module System

Modules encapsulate feature registrations:
```typescript
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)
```

### DI Container (InversifyJS)

```typescript
import { container } from '@pimcore/studio-ui-bundle'

// Bind
container.bind('MyServiceId').to(MyClass).inSingletonScope()
container.bind('MyServiceId').toConstantValue(myInstance)

// Retrieve
const service = container.get('MyServiceId')
```

## Step 2: Core Studio v2 Services

### Widget Registry
```typescript
import { serviceIds } from '@pimcore/studio-ui-bundle/app'
import { type WidgetRegistry } from '@pimcore/studio-ui-bundle/modules/widget-manager'

const widgetRegistry = container.get(serviceIds['WidgetRegistry'])
widgetRegistry.registerWidget({ name: 'my.widget', component: MyComponent })
```

### Dynamic Type Registry
```typescript
import { DynamicTypeObjectDataRegistry } from '@pimcore/studio-ui-bundle/modules/element'

const registry = container.get(
  serviceIds['DynamicTypes/ObjectDataRegistry']
)
registry.registerDynamicType(new MyDynamicType())
```

## Step 3: Dynamic Types (Pimcore Data Object Fields)

For custom field types in the class definition editor:

```typescript
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 
  }
}
```

**Register in `onInit()`** — never in `onStartup()`.

## Step 4: Building Components with Ant Design

### Form Component
```typescript
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 = ({ data, onChange }) => {
  const { t } = useTranslation()

  return (
    
      
         onChange({ ...data, name: e.target.value })} />
      
      
         onChange({ ...data, active: checked })} />
      
    
  )
}
```

### Table/List Component
```typescript
import React, { useEffect, useState } from 'react'
import { Table, Button, Space, Popconfirm } from 'antd'

export const MyList: React.FC = () => {
  const [data, setData] = useState([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    void (async () => {
      try { setData(await myApi.list()) }
      finally { setLoading(false) }
    })()
  }, [])

  return (
     v ? 'Yes' : 'No' },
      { title: 'Actions', render: (_, record) => (
         handleDelete(record.id)}>
          Delete
        
      )},
    ]} />
  )
}
```

### Modal / Dialog
```typescript
import { Modal, Form, Input } from 'antd'

const [open, setOpen] = useState(false)

 setOpen(false)}>
  
    
  

```

## Step 5: Select Components with Module-Level Caching

**ALWAYS use this pattern** for any Select that loads options from an API:

```typescript
import React from 'react'
import { Select, type SelectProps } from 'antd'

let cachedOptions: Array | null = null
let loadPromise: Promise | 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 = (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 
      (opt?.label ?? '').toLowerCase().includes(input.toLowerCase())
    } />
}
```

## Step 6: API Layer

```typescript
export interface MyEntity {
  id: number
  name: string
  active: boolean
}

export const myApi = {
  async list(): Promise {
    const res = await fetch('/pimcore-studio/api/my-bundle/entities')
    return (await res.json()).data ?? (await res.json())
  },
  async get(id: number): Promise {
    return (await fetch(`/pimcore-studio/api/my-bundle/entities/${id}`)).json()
  },
  async save(entity: Partial): Promise {
    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 {
    await fetch(`/pimcore-studio/api/my-bundle/entities/${id}`, { method: 'DELETE' })
  },
}
```

## Step 7: Translations

**File**: `Resources/translations/studio.en.yaml`
```yaml
my_entity: 'My Entity'
my_entity_name: 'Name'
```

**Usage**:
```typescript
import { useTranslation } from 'react-i18next'
const { t } = useTranslation()
t('my_entity_name')
```

## Step 8: Build System

- **Rsbuild** with Module Federation for plugin loading
- `npm run build` — Production build
- `npm run dev` — Development with hot reload

### package.json Template
```json
{
  "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"
  }
}
```

## Step 9: Directory Structure

```
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
```

## Reference

See `.claude/skills/pimcore-studio/patterns.md` for complete code templates.

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [cors-gmbh](https://github.com/cors-gmbh)
- **Source:** [cors-gmbh/pimcore-skills](https://github.com/cors-gmbh/pimcore-skills)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-cors-gmbh-pimcore-skills-pimcore-studio
- Seller: https://agentstack.voostack.com/s/cors-gmbh
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
