Header Actions System
Header Actions System Extensions can dynamically inject custom actions into the app's header and sub-header areas, providing seamless integration with the core interface. This demonstrates the incredible power of Enfyra's extension system - extensions can intervene and customize
Header Actions System
Extensions can dynamically inject custom actions into the app's header and sub-header areas, providing seamless integration with the core interface. This demonstrates the incredible power of Enfyra's extension system - extensions can intervene and customize ANY part of the application interface.
Live Demo: https://demo.enfyra.io/ - See header actions in action!
Table of Contents
- Overview
- Header Actions vs Sub-Header Actions
- Action Types
- Permission Integration
- Basic Usage
- Advanced Features
- Positioning and Layout
- Route-Based Actions
- Custom Components
- Real-World Examples
- Best Practices
- API Reference
Overview
** Extension Power: Extensions can completely control the application interface through header actions. This is just one example of how Enfyra extensions can intervene in ANY part of the app**, making them incredibly powerful for customization.
The Header Actions system provides two main areas for custom actions:
- Header Actions: Located in the top-right corner of the main header
- Sub-Header Actions: Located in the page sub-header, can be positioned left or right
Both systems are permission-aware and route-sensitive, allowing for sophisticated customization based on user roles and current page context.
Header Actions vs Sub-Header Actions
Header Actions (Top-Right Corner)
┌─────────────────────────────────────────────────────┐
│ Enfyra App [Header Actions] │
└─────────────────────────────────────────────────────┘
Sub-Header Actions (Page Level)
┌─────────────────────────────────────────────────────┐
│ [Left Actions] [Right Actions] │
└─────────────────────────────────────────────────────┘
│ Page Content │
Action Types
1. Button Actions
Standard buttons with icons, labels, and click handlers:
const buttonAction = {
id: 'export-data',
label: 'Export',
variant: 'solid',
color: 'primary',
onClick: () => exportData()
};
2. Custom Component Actions
Inject completely custom components:
const componentAction = {
id: 'custom-widget',
component: 'CustomWidget',
props: { data: dynamicData }
};
Permission Integration
** Every action is automatically wrapped with PermissionGate** - the same powerful permission system used throughout Enfyra.
** Learn about Permission System**
const permissionAction = {
id: 'admin-action',
label: 'Admin Panel',
onClick: () => openAdminPanel(),
permission: {
route: '/admin',
actions: ['read']
}
};
Permission Features: - Actions automatically hidden if user lacks permission - Supports complex AND/OR permission conditions - Works with role-based access control - Dynamic permission checking based on data context
** Complete Permission Guide**
Basic Usage
Header Actions in Extensions
<script setup>
const { register: registerHeaderActions } = useHeaderActionRegistry();
// Available globally in extensions
registerHeaderActions([
{
id: 'save-report',
label: 'Save Report',
color: 'success',
onClick: () => {
toast.add({
title: 'Report saved!',
color: 'success'
});
},
permission: {
route: '/reports',
actions: ['create']
}
}
]);
</script>
Sub-Header Actions in Extensions
<script setup>
const { register: registerSubHeaderActions } = useSubHeaderActionRegistry();
// Available globally in extensions
registerSubHeaderActions([
{
id: 'filter-toggle',
label: 'Filters',
side: 'left', // Position on left side
variant: 'soft',
onClick: () => toggleFilters()
}
]);
</script>
Advanced Features
Reactive Properties
All properties can be reactive using refs or computed:
<script setup>
const { register: registerHeaderActions } = useHeaderActionRegistry();
const isLoading = ref(false);
const itemCount = ref(0);
const dynamicLabel = computed(() =>
`Export (${itemCount.value} items)`
);
registerHeaderActions([
{
id: 'dynamic-export',
label: dynamicLabel,
loading: isLoading,
disabled: computed(() => itemCount.value === 0),
onClick: async () => {
isLoading.value = true;
await exportItems();
isLoading.value = false;
}
}
]);
</script>
Multiple Actions Registration
<script setup>
const { register: registerHeaderActions } = useHeaderActionRegistry();
const actions = [
{
id: 'refresh',
onClick: () => refresh()
},
{
id: 'export',
label: 'Export',
onClick: () => exportData()
},
{
id: 'settings',
onClick: () => openSettings()
}
];
// Register all at once
registerHeaderActions(actions);
</script>
Positioning and Layout
Sub-Header Positioning
<script setup>
const { register: registerSubHeaderActions } = useSubHeaderActionRegistry();
// Left side actions (typically filters, views)
registerSubHeaderActions([
{
id: 'view-toggle',
label: 'Grid View',
side: 'left',
onClick: () => toggleView()
},
{
id: 'export',
label: 'Export',
side: 'right', // Default
onClick: () => exportData()
}
]);
</script>
Responsive Behavior
Actions automatically adapt to screen size: - Mobile: Only icons shown - Desktop: Icons + labels shown - Tablet: Smaller buttons
Route-Based Actions
Show/Hide on Specific Routes
<script setup>
const { register: registerHeaderActions } = useHeaderActionRegistry();
registerHeaderActions([
{
id: 'route-specific',
label: 'Data Tools',
showOn: ['/data'], // Only show on data routes
hideOn: ['/settings'], // Hide on settings routes
onClick: () => openDataTools()
}
]);
</script>
Global vs Route-Specific Actions
<script setup>
const { register: registerHeaderActions } = useHeaderActionRegistry();
// Register multiple actions
registerHeaderActions([
{
id: 'global-help',
global: true, // Persists across all routes
onClick: () => openHelp()
},
{
id: 'page-specific',
label: 'Page Action', // Cleared when navigating away
onClick: () => doPageSpecificAction()
}
]);
</script>
Custom Components
Injecting Custom Components
<template>
<div id="my-extension">
<!-- Extension content -->
</div>
</template>
<script setup>
const { register: registerHeaderActions } = useHeaderActionRegistry();
// Define a custom component
const CustomStatusWidget = {
template: `
<div class="flex items-center gap-2">
<UBadge :color="status.color">{{ status.text }}</UBadge>
<UButton @click="refresh" variant="ghost" size="sm" />
</div>
`,
setup() {
const status = ref({ text: 'Online', color: 'green' });
const refresh = () => {
// Refresh logic
};
return { status, refresh };
}
};
// Register as header action
registerHeaderActions([
{
id: 'status-widget',
component: CustomStatusWidget
}
]);
</script>
Component with Props
<script setup>
const { register: registerHeaderActions } = useHeaderActionRegistry();
const DataCounter = {
template: `
<UBadge variant="soft" color="primary">
{{ count }} items
</UBadge>
`,
props: ['count']
};
const itemCount = ref(150);
registerHeaderActions([
{
id: 'data-counter',
component: DataCounter,
props: {
count: itemCount.value
}
}
]);
</script>
Real-World Examples
1. Data Export Extension
<template>
<div class="p-6">
<h2>Data Management</h2>
</div>
</template>
<script setup>
const { register: registerHeaderActions } = useHeaderActionRegistry();
const isExporting = ref(false);
const selectedFormat = ref('json');
const exportData = async () => {
isExporting.value = true;
try {
const { data } = await useApi('/export', {
method: 'POST',
body: { format: selectedFormat.value }
});
downloadFile(data.url);
toast.add({
title: 'Export completed',
description: `Data exported as ${selectedFormat.value.toUpperCase()}`,
color: 'success'
});
} catch (error) {
toast.add({
title: 'Export failed',
description: error.message,
color: 'error'
});
} finally {
isExporting.value = false;
}
};
// Export dropdown component
const ExportDropdown = {
template: `
<UDropdown :items="exportItems">
<UButton
label="Export"
:loading="loading"
variant="solid"
color="primary"
/>
</UDropdown>
`,
setup() {
const exportItems = [
[
{
label: 'JSON',
click: () => {
selectedFormat.value = 'json';
exportData();
}
},
{
label: 'CSV',
click: () => {
selectedFormat.value = 'csv';
exportData();
}
},
{
label: 'PDF',
click: () => {
selectedFormat.value = 'pdf';
exportData();
}
}
]
];
return {
exportItems,
loading: isExporting
};
}
};
// Register at top level - no onMounted needed
registerHeaderActions([
{
id: 'export-dropdown',
component: ExportDropdown,
permission: {
route: '/data',
actions: ['read']
}
}
]);
</script>
2. Real-time Status Dashboard
<template>
<div class="dashboard p-6">
<h1>System Dashboard</h1>
<!-- Dashboard content -->
</div>
</template>
<script setup>
const { register: registerSubHeaderActions } = useSubHeaderActionRegistry();
const connectionStatus = ref('connecting');
const lastUpdate = ref(null);
const autoRefresh = ref(true);
// Real-time connection monitoring
const StatusIndicator = {
template: `
<div class="flex items-center gap-2">
<UBadge
:color="badgeColor"
variant="soft"
class="animate-pulse"
>
{{ statusText }}
</UBadge>
<span class="text-xs text-gray-500">
{{ lastUpdateText }}
</span>
</div>
`,
setup() {
const badgeColor = computed(() => {
switch (connectionStatus.value) {
case 'connected': return 'green';
case 'connecting': return 'yellow';
case 'error': return 'red';
default: return 'gray';
}
});
const statusText = computed(() => {
return connectionStatus.value.charAt(0).toUpperCase() +
connectionStatus.value.slice(1);
});
const lastUpdateText = computed(() => {
if (!lastUpdate.value) return '';
return `Updated ${formatDistanceToNow(lastUpdate.value)} ago`;
});
return {
badgeColor,
statusText,
lastUpdateText
};
}
};
// Register status in sub-header left
registerSubHeaderActions([
{
id: 'connection-status',
component: StatusIndicator,
side: 'left'
}
]);
// Register controls in sub-header right
registerSubHeaderActions([
{
id: 'auto-refresh-toggle',
label: computed(() => autoRefresh.value ? 'Auto-refresh ON' : 'Auto-refresh OFF'),
variant: computed(() => autoRefresh.value ? 'solid' : 'outline'),
color: computed(() => autoRefresh.value ? 'primary' : 'neutral'),
side: 'right',
onClick: () => {
autoRefresh.value = !autoRefresh.value;
toast.add({
title: `Auto-refresh ${autoRefresh.value ? 'enabled' : 'disabled'}`,
color: autoRefresh.value ? 'success' : 'neutral'
});
}
}
]);
onMounted(() => {
// Monitor connection
watchConnection();
});
const watchConnection = () => {
const interval = setInterval(async () => {
try {
connectionStatus.value = 'connecting';
const { data } = await useApi('/health');
connectionStatus.value = 'connected';
lastUpdate.value = new Date();
} catch (error) {
connectionStatus.value = 'error';
}
}, autoRefresh.value ? 5000 : 30000);
onUnmounted(() => clearInterval(interval));
};
</script>
3. Advanced Permission-Based Actions
<template>
<div class="admin-panel p-6">
<h2>Administrative Tools</h2>
</div>
</template>
<script setup>
const { register: registerHeaderActions } = useHeaderActionRegistry();
const { register: registerSubHeaderActions } = useSubHeaderActionRegistry();
const { me } = useAuth();
const userRole = computed(() => me.value?.role?.name);
// Define actions at top level - no onMounted needed
const adminActions = [
{
id: 'system-settings',
label: 'System',
permission: {
route: '/admin',
actions: ['update']
},
onClick: () => navigateTo('/admin/system')
},
{
id: 'user-management',
label: 'Users',
permission: {
route: '/user_definition',
actions: ['create', 'update', 'delete']
},
onClick: () => navigateTo('/admin/users')
},
{
id: 'audit-logs',
label: 'Audit',
permission: {
route: '/audit_log',
actions: ['read']
},
onClick: () => navigateTo('/admin/audit')
}
];
// Super admin only actions
const superAdminActions = [
{
id: 'danger-zone',
label: 'Danger Zone',
color: 'error',
permission: {
and: [
{ route: '/admin', actions: ['delete'] },
{ allowedUsers: [me.value?.id] }
]
},
onClick: () => openDangerZone()
}
];
// Register actions
registerHeaderActions([...adminActions, ...superAdminActions]);
// Conditional registration based on user role
if (userRole.value === 'manager') {
registerSubHeaderActions([
{
id: 'team-overview',
label: `Team Overview (${me.value.team?.memberCount || 0})`,
side: 'left',
onClick: () => showTeamOverview()
}
]);
}
</script>
Best Practices
1. Use Meaningful IDs
// Good
{ id: 'export-user-data', label: 'Export Users' }
// Avoid
{ id: 'btn1', label: 'Export Users' }
2. Implement Permission Checks
// Always include permission conditions
{
id: 'delete-action',
label: 'Delete',
color: 'error',
permission: {
route: '/data',
actions: ['delete']
}
}
3. Provide Loading States
const isProcessing = ref(false);
{
id: 'process-data',
label: 'Process',
loading: isProcessing,
onClick: async () => {
isProcessing.value = true;
await processData();
isProcessing.value = false;
}
}
4. Use Appropriate Positioning
// Left side: Views, filters, status
{ side: 'left', id: 'filter-toggle' }
// Right side: Actions, exports, settings
{ side: 'right', id: 'export-data' }
5. Automatic Cleanup
// Actions are automatically cleaned up on route change
// Only actions with global: true persist across routes
// No manual cleanup needed in most cases
6. Cross-Reference Related Documentation
When working with permissions: - ** Permission Builder - Create complex permission rules - ** Permission Components - PermissionGate usage - ** Permission System** - Backend permission architecture
API Reference
HeaderAction Interface
interface HeaderAction {
// Core Properties
id: string; // Unique identifier
label?: string | ComputedRef<string>; // Button text
// Styling
variant?: 'solid' | 'outline' | 'ghost' | 'soft';
color?: 'primary' | 'secondary' | 'warning' | 'success' | 'info' | 'error' | 'neutral';
size?: 'sm' | 'md' | 'lg' | 'xl';
class?: string; // Custom CSS classes
// State
loading?: boolean | Ref<boolean> | ComputedRef<boolean>;
disabled?: boolean | Ref<boolean> | ComputedRef<boolean>;
show?: boolean | Ref<boolean> | ComputedRef<boolean>;
// Actions
onClick?: () => void; // Click handler
to?: string | Ref<string> | ComputedRef<string>; // Navigation target
submit?: () => void; // Submit handler
// Positioning & Visibility
side?: 'left' | 'right'; // Sub-header position (default: 'right')
showOn?: string[]; // Show on specific routes
hideOn?: string[]; // Hide on specific routes
global?: boolean; // Persist across routes
// Permissions
permission?: PermissionCondition; // Permission requirements
// Custom Components
component?: string | any; // Custom component
props?: Record<string, any>; // Component props
key?: string; // Force re-render key
}
useHeaderActionRegistry()
const { register: registerHeaderActions } = useHeaderActionRegistry();
// Accepts a single action object or an array of actions
registerHeaderActions(actionOrActions)
useSubHeaderActionRegistry()
const { register: registerSubHeaderActions } = useSubHeaderActionRegistry();
// Accepts a single action object or an array of actions
registerSubHeaderActions(actionOrActions)
Summary
The Header Actions system showcases the incredible power of Enfyra extensions - they can seamlessly integrate with and control ANY part of the application interface. This is just one example of how extensions can:
Intervene in core UI areas - Headers, sidebars, forms, tables
Inject custom functionality - Buttons, widgets, complete components
Respect permission systems - Automatic PermissionGate integration
Respond to route changes - Dynamic behavior based on current page
Provide real-time updates - Reactive properties and live data
** Extensions aren't limited to custom pages - they can enhance and customize every aspect of the Enfyra experience, making them incredibly powerful for building exactly what you need.**
Related Documentation: - Extension System - Complete extension development guide - Permission Components - PermissionGate and permission integration - API Integration - Fetching data for dynamic actions