一键导入
pimcore-studio-ui-context-menus
Adding and customizing context menu items in Pimcore Studio UI - ContextMenuRegistry, slots, providers, and priority system
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Adding and customizing context menu items in Pimcore Studio UI - ContextMenuRegistry, slots, providers, and priority system
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when creating a new color theme or token set for Pimcore Studio UI (a DynamicTypeThemeAbstract subclass registered in the ThemeRegistry), including choosing an extends chain, filling AntD color tokens, seeding the boot palette, and picking colors that meet WCAG AAA contrast
Building filters in Pimcore Studio UI - the declarative defineFilter framework for filter panels and sidebars (createFiltersStore, FiltersRenderer, useFilterQuery) and custom field-filter dynamic types for per-column listing filters (DynamicTypeFieldFilterAbstract, DynamicTypeFieldFilterRegistry). Use this whenever building, customizing, or extending any kind of filtering, filter panel, filter sidebar, or search/filter UI, or adding a new filter type to a listing or data view - even when the user only says "add filtering", "let users filter by X", "filter this list", or "build a filter sidebar".
Building listings in Pimcore Studio UI using the ListingBuilder decorator pattern - sorting, paging, filtering, inline editing, and custom decorators
UX and UI design conventions for Pimcore Studio - layout, spacing, action labels, writing style, and design principles for consistent extensions
Pimcore Studio migration and review checklist — 34-point verification checklist, anti-patterns to avoid, and reference base classes for validating Studio code completeness
Pimcore Studio PHP code style rules — strict types, class modifiers, final/readonly patterns, formatting (120 chars), named arguments, imports, PHPDoc, @throws documentation, and constructor promotion
| name | pimcore-studio-ui-context-menus |
| description | Adding and customizing context menu items in Pimcore Studio UI - ContextMenuRegistry, slots, providers, and priority system |
| metadata | {"audience":"pimcore-developers","focus":"ui-extension-points"} |
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.