skills/pimcore-studio-ui-context-menus/SKILL.md
Adding and customizing context menu items in Pimcore Studio UI - ContextMenuRegistry, slots, providers, and priority system
npx skillsauth add pimcore/skills pimcore-studio-ui-context-menusInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Adding and customizing context menus in Pimcore Studio UI: registering new items, modifying built-in ones, controlling priority/position, and hiding items conditionally.
BEFORE writing any import, read CRITICAL-IMPORT-PATHS.md.
All examples use bundle imports (@pimcore/studio-ui-bundle/*). For core development (@sdk/*, @Pimcore/*), see the referenced file.
Central registry that manages all context menu items. Each menu location is a slot, and you register providers into slots.
A slot is a named attachment point. Each tree, grid, or toolbar has one or more slots, available as constants on contextMenuConfig.
A provider implements ContextMenuItemProvider: a unique name, optional priority, and a useMenuItem React hook that returns the menu item (or null to hide it).
Lower number = higher position. Default is 999. Use contextMenuConfig priority constants to position relative to built-in entries.
import { container } from '@pimcore/studio-ui-bundle'
import { serviceIds } from '@pimcore/studio-ui-bundle/app'
import {
contextMenuConfig,
type ContextMenuRegistryInterface
} from '@pimcore/studio-ui-bundle/modules/app'
const contextMenuRegistry = container.get<ContextMenuRegistryInterface>(
serviceIds['App/ContextMenuRegistry/ContextMenuRegistry']
)
contextMenuRegistry.registerToSlot(slotName: string, provider: ContextMenuItemProvider): void
Example:
import React from 'react'
import { type AbstractModule, container } from '@pimcore/studio-ui-bundle'
import { serviceIds } from '@pimcore/studio-ui-bundle/app'
import { useAlertModal, Icon } from '@pimcore/studio-ui-bundle/components'
import {
contextMenuConfig,
type ContextMenuRegistryInterface,
type DataObjectTreeContextMenuProps
} from '@pimcore/studio-ui-bundle/modules/app'
import { useTranslation } from 'react-i18next'
export const DataObjectContextMenuExtension: AbstractModule = {
onInit () {
const contextMenuRegistry = container.get<ContextMenuRegistryInterface>(
serviceIds['App/ContextMenuRegistry/ContextMenuRegistry']
)
const config = contextMenuConfig.dataObjectTree
contextMenuRegistry.registerToSlot(config.name, {
name: 'custom-item',
priority: config.priority.rename - 1,
useMenuItem: (context: DataObjectTreeContextMenuProps) => {
const { t } = useTranslation()
const alertModal = useAlertModal()
// Conditional hiding: return null to hide for this context
if (context.target.type === 'folder') {
return null
}
return {
key: 'custom-item',
label: t('my-bundle.custom-item.label'),
icon: <Icon value="pimcore" />,
onClick: () => {
alertModal.info({
title: `Clicked for id: ${context.target.id}`,
content: 'Custom action.'
})
}
}
}
})
}
}
contextMenuRegistry.overrideSlotProvider(slotName: string, provider: ContextMenuItemProvider): void
Completely replaces an existing provider (matched by name).
const config = contextMenuConfig.dataObjectTree
contextMenuRegistry.overrideSlotProvider(config.name, {
name: 'rename', // must match existing provider name
priority: config.priority.rename,
useMenuItem: (context: DataObjectTreeContextMenuProps) => {
const { t } = useTranslation()
return {
key: 'rename',
label: t('data-object.custom-rename'),
icon: <Icon value="edit" />,
onClick: () => { /* custom rename */ }
}
}
})
contextMenuRegistry.updateSlotProvider(
slotName: string,
name: string,
updater: (existing: ContextMenuItemProvider) => ContextMenuItemProvider
): void
Wraps an existing provider. Always call the existing hook first and respect a null return so permission checks and built-in hide logic are preserved.
const config = contextMenuConfig.assetTree
contextMenuRegistry.updateSlotProvider(config.name, 'delete', (existingItem) => ({
...existingItem,
useMenuItem: (context: AssetTreeContextMenuProps) => {
const existingMenuItem = existingItem.useMenuItem(context)
const { confirm } = useFormModal()
const { t } = useTranslation()
if (existingMenuItem === null) {
return null
}
const originalOnClick = existingMenuItem.onClick
return {
...existingMenuItem,
onClick: () => {
confirm({
title: t('delete.confirmation.title'),
content: t('delete.confirmation.extra-warning'),
okText: t('delete'),
okButtonProps: { color: 'danger' },
onOk: () => { originalOnClick?.() }
})
}
}
}
}))
interface ContextMenuItemProvider {
name: string // Unique identifier within the slot
priority?: number // Lower = higher in menu (default: 999)
useMenuItem: (context: ContextProps) => ItemType | null // Return null to hide
}
name must be unique within a slot. Duplicate names cause the second registration to silently fail.priority defaults to 999. Use contextMenuConfig constants to position relative to built-in items.useMenuItem is a React hook — you can call other hooks inside it (useTranslation, useAlertModal, etc.). Return null to hide.MenuItemType)interface ItemType {
key: string // Unique key for React rendering
label: string | ReactNode // Display text
icon?: ReactNode // Icon component
onClick?: () => void // Click handler
disabled?: boolean // Greyed-out state
danger?: boolean // Red/danger styling
children?: ItemType[] // Submenu items
}
| Slot Constant | Slot Name | Context Type | Used In |
|---------------|-----------|--------------|---------|
| contextMenuConfig.documentTree.name | document.tree | DocumentTreeContextMenuProps | Document tree |
| contextMenuConfig.documentTreeAdvanced.name | document.tree.advanced | DocumentTreeContextMenuProps | Document tree (advanced submenu) |
| contextMenuConfig.documentEditorToolbar.name | document.editor.toolbar | DocumentEditorToolbarContextMenuProps | Document editor toolbar |
| Slot Constant | Slot Name | Context Type | Used In |
|---------------|-----------|--------------|---------|
| contextMenuConfig.dataObjectTree.name | data-object.tree | DataObjectTreeContextMenuProps | Data object tree |
| contextMenuConfig.dataObjectTreeAdvanced.name | data-object.tree.advanced | DataObjectTreeContextMenuProps | Data object tree (advanced submenu) |
| contextMenuConfig.dataObjectListGrid.name | data-object.list-grid | DataObjectListGridContextMenuProps | Data object list/grid view |
| Slot Constant | Slot Name | Context Type | Used In |
|---------------|-----------|--------------|---------|
| contextMenuConfig.assetTree.name | asset.tree | AssetTreeContextMenuProps | Asset tree |
| contextMenuConfig.assetListGrid.name | asset.list-grid | AssetListGridContextMenuProps | Asset grid view |
| contextMenuConfig.assetPreviewCard.name | asset.preview-card | AssetPreviewCardContextMenuProps | Asset preview cards |
| contextMenuConfig.assetEditorToolbar.name | asset.editor.toolbar | AssetEditorToolbarContextMenuProps | Asset editor toolbar |
All tree context menus share a similar shape:
interface TreeContextMenuProps {
target: {
id: number // Element ID
type: string // Element type (e.g., 'folder', 'page', 'image')
key: string // Tree node key
parentId?: number // Parent element ID
}
}
Specific types:
import {
type DocumentTreeContextMenuProps,
type DataObjectTreeContextMenuProps,
type AssetTreeContextMenuProps
} from '@pimcore/studio-ui-bundle/modules/app'
import {
type AssetListGridContextMenuProps,
type DataObjectListGridContextMenuProps
} from '@pimcore/studio-ui-bundle/modules/app'
Grid context props include the row data for the right-clicked item.
import {
type DocumentEditorToolbarContextMenuProps,
type AssetEditorToolbarContextMenuProps
} from '@pimcore/studio-ui-bundle/modules/app'
Toolbar context props include information about the currently open element.
999 (bottom)priority object with constants for built-in items:const config = contextMenuConfig.dataObjectTree
config.priority.open // Priority of built-in "Open"
config.priority.rename // Priority of built-in "Rename"
config.priority.delete // Priority of built-in "Delete"
config.priority.copy // ...
config.priority.paste
// Above rename
priority: config.priority.rename - 1
// Below rename
priority: config.priority.rename + 1
// At the very top
priority: 1
// At the bottom (default)
// omit priority
Grids work the same way — just use the grid slot constant and its context type:
const config = contextMenuConfig.assetListGrid
contextMenuRegistry.registerToSlot(config.name, {
name: 'grid-download-asset',
useMenuItem: (context: AssetListGridContextMenuProps) => {
const { t } = useTranslation()
return {
key: 'grid-download-asset',
label: t('asset.grid.download'),
icon: <Icon value="download" />
}
}
})
// DON'T - item still renders, just disabled
useMenuItem: (context) => ({
key: 'image-resize',
label: 'Resize Image',
disabled: context.target.type !== 'image'
})
// DO - return null to hide entirely
useMenuItem: (context) => {
if (context.target.type !== 'image') return null
return { key: 'image-resize', label: 'Resize Image' }
}
Duplicate name values in the same slot cause the second registration to silently fail. Always use unique, bundle-prefixed names (e.g., my-bundle-action-1).
// DON'T - hardcoded string
contextMenuRegistry.registerToSlot('dataObject.tree', provider)
// DO - use contextMenuConfig constants
contextMenuRegistry.registerToSlot(contextMenuConfig.dataObjectTree.name, provider)
// DON'T - magic number, may collide or drift
priority: 42
// DO - position relative to a known item
priority: config.priority.rename - 1
Always call existingItem.useMenuItem(context) and honor a null return. Skipping it breaks built-in permission checks and hide conditions.
Use useTranslation() inside useMenuItem; never hardcode menu labels.
tools
UX and UI design conventions for Pimcore Studio - layout, spacing, action labels, writing style, and design principles for consistent extensions
tools
Widget system in Pimcore Studio UI - registering widgets, opening them in layout areas, WidgetManagerTabConfig, and connecting widgets to navigation
tools
How bundles consume the Pimcore Studio UI SDK - plugins, modules, DI, registries, and imports
development
TypeScript coding standards and best practices for Pimcore Studio UI - type safety, null checks, and code quality