| name | pimcore-studio-ui-navigation |
| description | Adding main navigation entries and perspective permissions in Pimcore Studio UI - frontend registration and backend permission management |
| metadata | {"audience":"pimcore-developers","focus":"navigation-perspectives"} |
What This Skill Covers
How to add navigation entries to Pimcore Studio UI with perspective permissions:
- Registering main navigation items (frontend)
- perspectivePermission field and relation to perspectives
- Backend perspective permission registration (PHP)
- StudioContextPermissionsSubscriber pattern
- Translation keys for navigation and perspective editor
- Permission groups and naming conventions
When to Use This Skill
Use this when:
- Adding new features that need main navigation entries
- Creating bundle navigation items
- Setting up perspective permissions for features
- Controlling feature visibility per perspective
- Adding menu items to the sidebar
Writing Style for Navigation Labels
Navigation items use Title Case - each principal word is capitalized.
This applies to:
- Main navigation menu items
- Tab titles when menu items are opened
- Context menu labels
- Section headings
- Widget titles
Title Case Examples
mainNavRegistry.registerMainNavItem({
path: 'Data Management/Custom Reports',
label: 'custom-reports.title'
})
Navigation vs Buttons
| Element Type | Case Style | Example |
|---|
| Navigation items | Title Case | "Custom Reports" |
| Menu items | Title Case | "Output Channels" |
| Tab titles | Title Case | "Data Objects" |
| Buttons | Sentence case | "Export CSV" |
| Actions | Sentence case | "Save draft" |
Remember: Navigation = Title Case, Buttons/Actions = Sentence case
For button and action labels, see pimcore-studio-ui-buttons skill.
🚨 Import Paths
BEFORE writing any import, read CRITICAL-IMPORT-PATHS.md.
All examples below use bundle imports (@pimcore/studio-ui-bundle/*). For core development (@sdk/*, @Pimcore/*), see the referenced file.
Navigation Registration (Frontend)
Navigation items are registered in your module's initialization file using the MainNavRegistry.
Basic Pattern
import { type AbstractModule } from '@pimcore/studio-ui-bundle'
import { container, serviceIds } from '@pimcore/studio-ui-bundle/app'
import { type MainNavRegistry } from '@pimcore/studio-ui-bundle/modules/app'
export const YourFeatureModule: AbstractModule = {
onInit: (): void => {
const mainNavRegistry = container.get<MainNavRegistry>(serviceIds.mainNavRegistry)
mainNavRegistry.registerMainNavItem({
path: 'ParentGroup/SubGroup/Your Feature',
label: 'your-feature.navigation.title',
order: 5,
perspectivePermission: 'dataManagement.yourFeature',
widgetConfig: {
name: 'Your Feature',
id: 'your-feature',
component: 'your-feature',
config: {
translationKey: 'your-feature.navigation.title',
icon: {
type: 'name',
value: 'your-icon'
}
}
}
})
}
}
Navigation Item Properties
interface IMainNavItem {
path: string
label?: string
order?: number
perspectivePermission?: string
widgetConfig?: WidgetManagerTabConfig
id?: string
icon?: string
groupIcon?: string
group?: string
dividerBottom?: boolean
children?: IMainNavItem[]
permission?: string
perspectivePermissionHide?: string
className?: string
hidden?: () => boolean
}
Real-World Example: Target Groups
export const TargetGroupsModule: AbstractModule = {
onInit: (): void => {
const mainNavRegistry = container.get<MainNavRegistry>(serviceIds.mainNavRegistry)
mainNavRegistry.registerMainNavItem({
path: 'ExperienceEcommerce/PersonalisationTargeting/Target Groups',
label: 'personalization.target-groups',
order: 10,
permission: 'targeting',
perspectivePermission: 'experienceEcommerce.personalizationTargetGroups',
widgetConfig: {
name: 'Target Groups',
id: 'target-groups',
component: 'target-groups-container',
config: {
translationKey: 'personalization.target-groups',
icon: {
type: 'name',
value: 'target-group'
}
}
}
})
}
}
perspectivePermission Field
The perspectivePermission field controls which perspectives can see the navigation item.
Format: Dot Notation
perspectivePermission: '{group}.{permissionKey}'
- First part: Group name (must match
ContextPermissionGroups enum)
- Second part: Permission key (registered in PHP)
Available Permission Groups
enum ContextPermissionGroups: string
{
case QUICK_ACCESS = 'quickAccess';
case DATA_MANAGEMENT = 'dataManagement';
case EXPERIENCE_ECOMMERCE = 'experienceEcommerce';
case ASSET_MANAGEMENT = 'assetManagement';
case TRANSLATIONS = 'translations';
case REPORTING = 'reporting';
case SYSTEM = 'system';
case SEARCH = 'search';
}
Permission Examples
perspectivePermission: 'experienceEcommerce.personalizationTargetingRules'
perspectivePermission: 'experienceEcommerce.personalizationTargetGroups'
perspectivePermission: 'experienceEcommerce.portalEngine'
perspectivePermission: 'experienceEcommerce.emails'
perspectivePermission: 'dataManagement.portalEngineCollections'
perspectivePermission: 'dataManagement.bookmarkLists'
perspectivePermission: 'dataManagement.tagConfiguration'
perspectivePermission: 'reporting.dashboards'
perspectivePermission: 'quickAccess.open_asset'
perspectivePermission: 'quickAccess.open_document'
perspectivePermission: 'system.users'
perspectivePermission: 'system.roles'
How It Works
- Frontend checks
perspectivePermission against active perspective
- Backend provides list of enabled permissions per perspective
- Navigation filters items based on permission check
- Perspective Editor uses these permissions to configure perspectives
const isAllowedInPerspective = (permission: string): boolean => {
const activePerspective = selectActivePerspective(store.getState())
if (!activePerspective) return false
return isPathTrue(activePerspective.contextPermissions, permission)
}
Backend Permission Registration (PHP)
🚨 IMPORTANT: Where to Register Permissions
The approach differs based on which bundle you're working in:
studio-ui-bundle (Core)
If you're adding permissions for studio-ui-bundle features, register them directly in studio-backend-bundle:
Primary Location (Most Permissions):
Additional Permissions via Subscribers:
- Additional permissions can be registered via event subscribers in studio-backend-bundle
- Multiple context-specific subscribers add their permissions via
ContextPermissionsServiceInterface
Custom Bundles (e.g., personalization-bundle, your-bundle)
If you're creating a custom bundle (first-party or third-party), register permissions in your own bundle using a StudioContextPermissionsSubscriber as shown below.
For studio-ui-bundle: Add to ContextPermissionService
Most core studio-ui-bundle permissions are defined directly in the ContextPermissionService:
<?php
namespace Pimcore\Bundle\StudioBackendBundle\Perspective\Service;
final readonly class ContextPermissionService implements ContextPermissionsServiceInterface
{
private function getDefaultPermissions(): array
{
return [
new ContextPermissionData(
'open_asset',
ContextPermissionGroups::QUICK_ACCESS->value,
true
),
new ContextPermissionData(
'open_document',
ContextPermissionGroups::QUICK_ACCESS->value,
true
),
new ContextPermissionData(
'yourNewFeature',
ContextPermissionGroups::DATA_MANAGEMENT->value,
true
),
];
}
}
For Custom Bundles: Create a StudioContextPermissionsSubscriber
Permissions for custom bundles must be registered using a StudioContextPermissionsSubscriber in your bundle.
Step 1: Create the Subscriber
<?php
namespace YourVendor\YourBundle\EventSubscriber;
use Pimcore\Bundle\StudioBackendBundle\Perspective\Model\ContextPermissionData;
use Pimcore\Bundle\StudioBackendBundle\Perspective\Service\ContextPermissionsServiceInterface;
use Pimcore\Bundle\StudioBackendBundle\Perspective\Util\Constant\ContextPermissionGroups;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
final readonly class StudioContextPermissionsSubscriber implements EventSubscriberInterface
{
public function __construct(
private ContextPermissionsServiceInterface $permissionsService,
) {
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::CONTROLLER => 'addContextPermissions',
];
}
public function addContextPermissions(): void
{
$this->permissionsService->add(
new ContextPermissionData(
'yourFeature', // Permission key
ContextPermissionGroups::DATA_MANAGEMENT->value, // Group
true // Default enabled
)
);
}
}
Step 2: Register as Service (Auto-configured)
Most Symfony bundles auto-configure event subscribers. If not, add to services.yaml:
services:
YourVendor\YourBundle\EventSubscriber\StudioContextPermissionsSubscriber:
tags:
- { name: kernel.event_subscriber }
ContextPermissionData Parameters
new ContextPermissionData(
string $key,
string $group,
bool $defaultValue = true
)
Real-World Example: Personalization Bundle
<?php
namespace Pimcore\Bundle\PersonalizationBundle\EventSubscriber;
use Pimcore\Bundle\StudioBackendBundle\Perspective\Model\ContextPermissionData;
use Pimcore\Bundle\StudioBackendBundle\Perspective\Service\ContextPermissionsServiceInterface;
use Pimcore\Bundle\StudioBackendBundle\Perspective\Util\Constant\ContextPermissionGroups;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
final readonly class StudioContextPermissionsSubscriber implements EventSubscriberInterface
{
public function __construct(
private ContextPermissionsServiceInterface $permissionsService,
) {
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::CONTROLLER => 'addContextPermissions',
];
}
public function addContextPermissions(): void
{
$this->permissionsService->add(
new ContextPermissionData(
'personalizationTargetingRules',
ContextPermissionGroups::EXPERIENCE_ECOMMERCE->value
)
);
$this->permissionsService->add(
new ContextPermissionData(
'personalizationTargetGroups',
ContextPermissionGroups::EXPERIENCE_ECOMMERCE->value
)
);
}
}
Translation Keys
You need two sets of translation keys:
1. Navigation Item Label
Used in the main navigation menu.
your-feature:
navigation:
title: Your Feature Name
2. Perspective Editor Label
Used in the Perspective Editor for enabling/disabling the feature.
perspective-editor:
form:
main-nav-permission:
dataManagement:
yourFeature: Data Management > Your Feature Name
Real-World Example: Target Groups
personalization:
target-groups: Target Groups
perspective-editor:
form:
main-nav-permission:
experienceEcommerce:
personalizationTargetGroups: Personalisation / Targeting > Target Groups
Group Category Labels
If adding a new group category label:
perspective-editor:
form:
main-nav-permission:
category:
experienceEcommerce: Experience & E-commerce
dataManagement: Data Management
reporting: Reporting
Complete Flow Diagram
┌─────────────────────────────────────────────────────────────┐
│ 1. FRONTEND: Register Navigation Item │
│ │
│ mainNavRegistry.registerMainNavItem({ │
│ path: 'Group/SubGroup/Item', │
│ perspectivePermission: 'group.permissionKey' │
│ }) │
└────────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. BACKEND: Register Permission (PHP) │
│ │
│ StudioContextPermissionsSubscriber::addContextPermissions()│
│ → permissionsService->add( │
│ new ContextPermissionData('permissionKey', 'group') │
│ ) │
└────────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. TRANSLATIONS: Add Translation Keys │
│ │
│ - your-feature.navigation.title │
│ - perspective-editor.form.main-nav-permission.group.key │
└────────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. PERSPECTIVE CONFIGURATION │
│ │
│ User enables/disables in Perspective Editor │
│ Settings stored per perspective │
└────────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 5. RUNTIME: Navigation Filtering │
│ │
│ - Frontend checks perspectivePermission │
│ - Only shows nav items with enabled permissions │
└─────────────────────────────────────────────────────────────┘
Testing Your Navigation Item
1. Verify Registration
Check browser console for your module initialization:
console.log('YourFeatureModule initialized')
2. Check Perspective Editor
- Navigate to System > Perspective Editor
- Select or create a perspective
- Expand the appropriate group (e.g., "Experience & E-commerce")
- Find your permission checkbox
- Verify translation label is correct
3. Toggle Perspective Permission
- Enable your permission in the perspective
- Save perspective
- Reload Studio
- Verify navigation item appears
- Disable permission and verify item disappears
4. Check Translation
- Change language in user settings
- Verify navigation label updates correctly
- Check perspective editor label in different language
Common Patterns
Pattern 1: Simple Feature Navigation
mainNavRegistry.registerMainNavItem({
path: 'DataManagement/Your Feature',
label: 'your-feature.title',
perspectivePermission: 'dataManagement.yourFeature',
widgetConfig: {
name: 'Your Feature',
id: 'your-feature',
component: 'your-feature',
config: {
translationKey: 'your-feature.title',
icon: { type: 'name', value: 'your-icon' }
}
}
})
Pattern 2: Multiple Related Items
mainNavRegistry.registerMainNavItem({
path: 'ExperienceEcommerce/YourBundle/Feature 1',
label: 'your-bundle.feature1',
order: 10,
perspectivePermission: 'experienceEcommerce.feature1',
widgetConfig: { }
})
mainNavRegistry.registerMainNavItem({
path: 'ExperienceEcommerce/YourBundle/Feature 2',
label: 'your-bundle.feature2',
order: 20,
perspectivePermission: 'experienceEcommerce.feature2',
widgetConfig: { }
})
Pattern 3: With User Permission Check
mainNavRegistry.registerMainNavItem({
path: 'System/Your Admin Feature',
label: 'your-feature.admin',
permission: 'admin',
perspectivePermission: 'system.yourAdminFeature',
widgetConfig: { }
})
Common Mistakes
❌ Missing Backend Registration
mainNavRegistry.registerMainNavItem({
perspectivePermission: 'dataManagement.yourFeature'
})
❌ Wrong Group Name
new ContextPermissionData(
'yourFeature',
'dataManagment'
)
new ContextPermissionData(
'yourFeature',
ContextPermissionGroups::DATA_MANAGEMENT->value
)
❌ Mismatched Permission Keys
perspectivePermission: 'dataManagement.yourFeature'
new ContextPermissionData('your-feature', ...)
❌ Missing Translation Keys
your-feature:
navigation:
title: Your Feature
your-feature:
navigation:
title: Your Feature
perspective-editor:
form:
main-nav-permission:
dataManagement:
yourFeature: Data Management > Your Feature
Quick Reference Checklist
To add a navigation item with perspective permissions:
Frontend (TypeScript):
Backend (PHP):
Translations:
Testing:
Next Steps