Extension System
Extension System The Extension System allows you to create custom pages, widgets, and global shell integrations using Vue.js components. Page extensions pair with menu items to provide route content, widget extensions are embedded inside pages, and global extensions mount once at
Extension System
The Extension System allows you to create custom pages, widgets, and global shell integrations using Vue.js components. Page extensions pair with menu items to provide route content, widget extensions are embedded inside pages, and global extensions mount once at the app shell level for app-wide registration logic.
** Menu Management Guide** - Learn how to create and configure menus
Table of Contents
- Understanding Extensions and Menus
- Complete Workflow Example
- Extension Types
- Full SDK Access
- Complete List of Injected Resources
- UI Components
- Enfyra Composables
- Nuxt Composables
- Vue 3 Composition API
- Browser APIs
- Advanced Extension Features
- Header Actions Integration
- Widget System
- Global Extension System
- File Upload Support
- Using NPM Packages in Extensions
- Extension Management
- Best Practices
- Common Issues and Solutions
- Advanced Patterns
- Security Considerations
- Summary
Understanding Extensions and Menus
The Problem
When you create a menu item with a custom path like /reports/sales, clicking it leads to an empty page because there's no code to handle that route.
The Solution
Extensions provide the Vue.js component that renders when users navigate to your custom menu path. This creates a complete user experience: 1. Menu = Navigation entry point 2. Extension = The actual page content
Complete Workflow Example: From Menu to Extension Display
This example shows the complete process from creating a menu to displaying custom content.
Step 1: Create a Menu Item
- Navigate to Settings > Menus
- Click "Create" to add a new menu
- Configure your menu:
- Type: Select "Menu" (for regular menu items)
- Label: "Analytics Dashboard"
- Path:
/custom/analytics(this will be your extension's URL) - Icon: Choose ""
- Sidebar: Select "Dashboard" (so it appears under Dashboard sidebar)
- Save the menu item
Result: You now have a menu entry, but clicking it shows a blank page because there's no extension linked.
Step 2: Create the Extension
- Navigate to Settings > Extensions
- Click "Create Extension"
- Fill in the extension details:
- Name: "Analytics Dashboard"
- Extension ID: Auto-generated (e.g., "analytics-dashboard-1234")
- Type: Select "Page" (for menu-linked extensions)
- Description: "Custom analytics dashboard with charts and metrics"
- Menu: Use the relation picker to select the menu you just created
- Version: "1.0.0"
- Is Enabled: Check this box
Step 3: Write Your Extension Code
In the code editor, write your Vue.js Single File Component (SFC).
** Complete Copy-Paste Ready Example:** This example demonstrates all features and can be pasted directly into the extension editor:
<template>
<div class="p-6 space-y-6">
<!-- Header Section -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold">Dashboard Example</h1>
<p class="text-gray-500 dark:text-gray-400">
Complete working example - copy and paste this code
</p>
</div>
<UBadge color="green" variant="soft">
Live Data
</UBadge>
</div>
<!-- Stats Cards Grid -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<UCard>
<div class="text-center p-4">
<div class="text-2xl font-bold text-blue-600">{{ stats.users }}</div>
<div class="text-sm text-gray-500">Total Users</div>
</div>
</UCard>
<UCard>
<div class="text-center p-4">
<div class="text-2xl font-bold text-green-600">{{ stats.revenue }}</div>
<div class="text-sm text-gray-500">Monthly Revenue</div>
</div>
</UCard>
<UCard>
<div class="text-center p-4">
<div class="text-2xl font-bold text-purple-600">{{ stats.orders }}</div>
<div class="text-sm text-gray-500">Orders Today</div>
</div>
</UCard>
</div>
<!-- Action Buttons -->
<div class="flex flex-wrap gap-4">
<UButton
@click="refreshData"
:loading="loading"
color="primary"
>
Refresh Data
</UButton>
<UButton
@click="fetchFromAPI"
:loading="apiLoading"
variant="outline"
>
Fetch Real Data
</UButton>
<PermissionGate :condition="{ route: '/admin', actions: ['create'] }">
<UButton
@click="generateReport"
variant="soft"
color="green"
>
Generate Report (Admin Only)
</UButton>
</PermissionGate>
</div>
<!-- Data Table -->
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h3 class="text-lg font-semibold">Recent Activity</h3>
<UBadge variant="soft">{{ recentActivity.length }} items</UBadge>
</div>
</template>
<UTable :rows="recentActivity" :columns="columns">
<template #action-data="{ row }">
<div class="flex items-center gap-2">
{{ row.action }}
</div>
</template>
<template #time-data="{ row }">
<UBadge variant="soft" size="xs">{{ row.time }}</UBadge>
</template>
</UTable>
</UCard>
<!-- Form Example -->
<UCard>
<template #header>
<h3 class="text-lg font-semibold">Quick Add Form</h3>
</template>
<div class="space-y-4">
<UInput
v-model="formData.name"
placeholder="Enter name"
label="Name"
/>
<UTextarea
v-model="formData.description"
placeholder="Enter description"
label="Description"
:rows="3"
/>
<USelect
v-model="formData.category"
:options="categories"
label="Category"
placeholder="Select category"
/>
<div class="flex items-center gap-4">
<USwitch v-model="formData.isActive" label="Active" />
<UCheckbox v-model="formData.isPublic" label="Public" />
</div>
<UButton
@click="submitForm"
color="primary"
block
:disabled="!isFormValid"
>
Submit Form
</UButton>
</div>
</UCard>
</div>
</template>
<script setup>
const {
register: registerHeaderActions,
unregister: unregisterHeaderAction
} = useHeaderActionRegistry();
// ==========================================
// ALL FUNCTIONS ARE GLOBALLY AVAILABLE
// No imports needed - just use them directly!
// ==========================================
// Nuxt & Enfyra Composables - Available globally
const toast = useToast();
const { me } = useAuth();
const router = useRouter();
const route = useRoute();
// Vue Composition API - Available globally
const loading = ref(false);
const apiLoading = ref(false);
// Reactive state
const stats = reactive({
users: 1234,
revenue: '$12,450',
orders: 89
});
const recentActivity = ref([
{ id: 1, action: 'User login', user: '[email protected]', time: '2 mins ago' },
{ id: 2, action: 'New order', user: '[email protected]', time: '5 mins ago' },
{ id: 3, action: 'Payment received', user: '[email protected]', time: '10 mins ago' },
{ id: 4, action: 'Profile update', user: '[email protected]', time: '15 mins ago' },
{ id: 5, action: 'Password reset', user: '[email protected]', time: '20 mins ago' }
]);
// Table columns configuration
const columns = [
{ key: 'action', label: 'Action', sortable: true },
{ key: 'user', label: 'User', sortable: true },
{ key: 'time', label: 'Time' }
];
// Form data
const formData = reactive({
name: '',
description: '',
category: null,
isActive: true,
isPublic: false
});
const categories = [
{ value: 'product', label: 'Product' },
{ value: 'service', label: 'Service' },
{ value: 'other', label: 'Other' }
];
// Computed properties
const isFormValid = computed(() => {
return formData.name && formData.description && formData.category;
});
// Helper function for action icons
const getActionIcon = (action) => {
const icons = {
'User login': '',
'New order': '',
'Payment received': '',
'Profile update': '',
'Password reset': ''
};
return icons[action] || '';
};
// Methods
const refreshData = async () => {
loading.value = true;
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 1000));
// Update stats with random values
stats.users += Math.floor(Math.random() * 10);
stats.orders += Math.floor(Math.random() * 5);
toast.add({
title: 'Success',
description: 'Data has been refreshed',
color: 'green',
});
loading.value = false;
};
// Fetch real data from API
const fetchFromAPI = async () => {
apiLoading.value = true;
// useApi already handles errors - no try-catch needed
const { data, error } = await useApi('/user_definition', {
query: {
limit: 5,
fields: 'id,email,created_at'
}
});
if (error.value) {
toast.add({
title: 'API Error',
description: error.value.message || 'Failed to fetch data',
color: 'red'
});
} else if (data.value?.data) {
// Update activity with real data
recentActivity.value = data.value.data.map((user, index) => ({
id: user.id,
action: 'User registered',
user: user.email,
time: `${index + 1} days ago`
}));
toast.add({
title: 'Data Loaded',
description: `Fetched ${data.value.data.length} records from API`,
color: 'green'
});
}
apiLoading.value = false;
};
const generateReport = () => {
toast.add({
title: 'Report Generated',
description: 'Your report is ready for download',
color: 'blue',
timeout: 5000,
actions: [{
label: 'Download',
color: 'white',
click: () => {
toast.add({
title: 'Downloading...',
description: 'Report download started'
});
}
}]
});
};
const submitForm = async () => {
if (!isFormValid.value) return;
// Example: Send data to API with proper error handling
// const { data, error } = await useApi('/my-endpoint', {
// method: 'POST',
// body: formData
// });
//
// if (error.value) {
// toast.add({
// title: 'Submission Failed',
// description: error.value.message,
// color: 'red'
// });
// return;
// }
toast.add({
title: 'Form Submitted',
description: `Created: ${formData.name}`,
color: 'green'
});
// Reset form
formData.name = '';
formData.description = '';
formData.category = null;
formData.isActive = true;
formData.isPublic = false;
};
// Register header actions when component mounts
onMounted(() => {
// Add custom actions to app header
registerHeaderActions([
{
id: 'refresh-dashboard',
label: 'Refresh',
onClick: refreshData,
color: 'primary',
variant: 'soft'
},
{
id: 'view-settings',
label: 'Settings',
variant: 'ghost',
onClick: () => {
navigateTo('/settings');
}
}
]);
// Log current user info
if (me.value) {
console.log('Extension loaded for user:', me.value.email);
}
// Example: Check permissions
const { hasPermission } = usePermissions();
if (hasPermission('/admin', 'GET')) {
console.log('User has admin read access');
}
});
// Cleanup when component unmounts
onUnmounted(() => {
// Unregister header actions
unregisterHeaderAction('refresh-dashboard');
unregisterHeaderAction('view-settings');
});
</script>
<style scoped>
/* Add any custom styles here if needed */
/* Tailwind classes are recommended */
</style>
Step 4: Save and Test
- Click "Create" to save your extension
- The system automatically compiles your Vue code
- Navigate to your custom menu path:
/custom/analytics - Your extension content now displays!
Step 5: The Complete Flow
What happens when user clicks the menu:
- User clicks "Analytics Dashboard" in the menu
- Browser navigates to
/custom/analytics - Enfyra's dynamic router catches the route
- System queries for menu with path
/custom/analytics - Finds linked extension through the one-to-one relationship
- Loads and compiles the extension Vue SFC code
- Renders extension with full access to components and composables
- User sees the custom analytics dashboard content
The Magic: Menu provides navigation Extension provides content Complete user experience!
Extension Types
Page Extensions
- Purpose: Full-page applications linked to menu items
- Usage: Selected through menu relation picker
- Example: Dashboards, reports, custom forms
Widget Extensions
- Purpose: Reusable components for embedding anywhere
- Usage: Can be embedded using
<Widget :id="databaseId" /> - Example: Charts, status cards, mini-forms
Global Extensions
- Purpose: App-wide shell registration and background realtime behavior
- Usage: Create an extension with type
global; eApp mounts enabled global extensions invisibly during layout init - Example: Notification bell in the account panel, global unread counters, admin socket listeners, background refresh bridges
Full SDK Access in Extensions
Complete SDK Integration: Extensions run inside the Nuxt-based admin app and have full access to all Enfyra Nuxt SDK features from @enfyra/sdk-nuxt. Every composable, utility, and API feature available in the main application is also available in your extensions – no limitations.
Complete List of Injected Resources
Extensions have access to a comprehensive set of resources that are automatically injected at runtime:
UI Components (Auto-Injected)
All UI components are automatically injected by the extension system and can be used directly in templates without imports:
Nuxt UI Components ( Nuxt UI Documentation):
- UIcon, Icon - Icons and SVG components
- UButton - Buttons with variants and states
- UCard - Container cards with headers/footers
- UBadge - Status badges and labels
- UInput - Text input fields
- UTextarea - Multi-line text areas
- USelect - Dropdown selection
- UCheckbox - Checkbox inputs
- USwitch - Toggle switches
- UModal - Modal dialogs
- UPopover - Popover overlays
- UTooltip - Hover tooltips
- UAlert - Alert messages
- UAvatar - User avatars
- UProgress - Progress indicators
- UTable - Data tables
- UPagination - Page navigation
- UBreadcrumb - Breadcrumb navigation
- UTabs - Tab interfaces
- UAccordion - Collapsible content
- UForm - Form containers
Custom Enfyra Components:
- DataTable - Advanced data tables with filtering
- PermissionGate - Permission-based content visibility
- FormEditor - Dynamic form generation
- FilterDrawer - Advanced filtering interface
- LoadingState - Loading state indicators
- EmptyState - Empty state displays
- SettingsCard - Settings interface cards
- Image - Enhanced image component
- UploadModal - File upload interface
- Widget - Dynamic widget embedding
<template>
<!-- Components are injected and can be used directly -->
<UCard class="p-6 space-y-4">
<!-- Form Elements -->
<UInput v-model="name" placeholder="Enter name" />
<UTextarea v-model="description" placeholder="Enter description" />
<USelect v-model="selectedOption" :options="options" />
<USwitch v-model="enabled" label="Enable feature" />
<!-- Buttons and Actions -->
<UButton @click="handleClick" color="primary">
Click Me
</UButton>
<!-- Data Display -->
<UTable :rows="data" :columns="columns" />
<UBadge color="green">Status: Active</UBadge>
<!-- Advanced Components -->
<PermissionGate :condition="{ route: '/users', actions: ['read'] }">
<UButton variant="outline">Admin Only Button</UButton>
</PermissionGate>
</UCard>
</template>
<script setup>
// Components are automatically available in template - no need to import
// All composables and Vue functions are available globally - use directly
const name = ref('');
const description = ref('');
const enabled = ref(false);
const selectedOption = ref(null);
const options = ['Option 1', 'Option 2', 'Option 3'];
const data = [
{ id: 1, name: 'Item 1', status: 'Active' },
{ id: 2, name: 'Item 2', status: 'Inactive' }
];
const columns = [
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'Name' },
{ key: 'status', label: 'Status' }
];
const handleClick = () => {
console.log('Button clicked!');
};
</script>
Enfyra Composables (Global Access)
All Enfyra composables are automatically injected and available globally:
API & Data:
- useApi() - Custom API wrapper with error handling (recommended)
- useSchema() - Schema validation and form generation
- useFilterQuery() - Advanced filtering and querying Filter System Guide
- useDataTableColumns() - Data table column management
Authentication & Permissions:
- useAuth() - Authentication state and methods (me, isLoggedIn, login, logout)
- usePermissions() - Permission checking and validation
UI & State Management:
- useHeaderActionRegistry() - Register header actions
- useSubHeaderActionRegistry() - Register sub-header actions
- useAccountPanelRegistry() - Register rows in the sidebar account panel
- useScreen() - Screen size and responsive utilities
- useGlobalState() - Global state management
- useConfirm() - Confirmation dialogs
Nuxt Composables (Global Access)
All Nuxt composables are available without import:
Navigation & Routing:
- useRoute() - Current route information
- useRouter() - Router instance for navigation
- navigateTo() - Programmatic navigation
State Management:
- useState() - Nuxt state management
- useCookie() - Cookie management
Data Fetching:
- useFetch() - Server-side data fetching
- useAsyncData() - Async data handling
- useLazyFetch() - Lazy data loading
Meta & SEO:
- useHead() - Document head management
- useSeoMeta() - SEO metadata
App Context:
- useNuxtApp() - Nuxt app instance
- useToast() - Toast notifications
Vue 3 Composition API (Global Access)
Complete Vue 3 Composition API is available globally:
Core Reactivity:
- ref() - Create reactive references
- reactive() - Create reactive objects
- computed() - Computed properties
- readonly() - Read-only reactive data
- shallowRef() - Shallow reactive references
- shallowReactive() - Shallow reactive objects
Lifecycle Hooks:
- onMounted() - Component mounted
- onUnmounted() - Component unmounted
- onBeforeMount() - Before component mount
- onBeforeUnmount() - Before component unmount
- onUpdated() - Component updated
- onBeforeUpdate() - Before component update
Watchers:
- watch() - Watch reactive data
- watchEffect() - Effect-based watching
Component Composition:
- defineProps() - Define component props
- defineEmits() - Define component events
- defineExpose() - Expose component methods
- defineComponent() - Define Vue component
- h() - Render function helper
- resolveComponent() - Resolve component by name
Utilities:
- nextTick() - Wait for DOM updates
- toRef() - Convert to ref
- toRefs() - Convert to refs
- unref() - Unwrap ref value
- isRef() - Check if value is ref
- markRaw() - Mark as non-reactive
- toRaw() - Get raw object
- isProxy(), isReactive(), isReadonly() - Type checking
- effectScope() - Effect scope management
- getCurrentScope() - Get current scope
- onScopeDispose() - Scope cleanup
Browser APIs (Available)
Standard browser APIs are accessible:
- fetch() - HTTP requests
- console - Console logging
- window - Window object
- document - DOM manipulation
For complete API usage examples, see API Integration Guide
Basic injected resources usage example:
<script setup>
// All functions and composables are available globally - just use them directly!
// API Access
const { data } = await useApi('/extension_definition', {
query: { limit: 10 }
});
// Authentication
const { me, isLoggedIn, login, logout } = useAuth();
// Permissions
const { hasPermission } = usePermissions();
if (hasPermission('/users', 'POST')) {
// User can create
}
// Notifications
const toast = useToast();
toast.add({
title: 'Success!',
color: 'success'
});
// Navigation
const router = useRouter();
const route = useRoute();
// Schema & Validation
const { validate, generateEmptyForm } = useSchema('extension_definition');
// Vue 3 Composition API - use directly from global
const loading = ref(false);
const state = reactive({ count: 0 });
const doubled = computed(() => state.count * 2);
// State management
const globalState = useState('myExtension', () => ({}));
</script>
Permission Gates
Control visibility based on permissions:
<template>
<PermissionGate :condition="{
route: '/users',
actions: ['create']
}">
<UButton>Admin Only Button</UButton>
</PermissionGate>
</template>
Advanced Extension Features
Header Actions Integration
** Extensions can inject custom actions directly into the app's header and sub-header areas** - demonstrating the incredible power to intervene in ANY part of the application interface.
Quick Header Action Example
<script setup>
const { register: registerHeaderActions } = useHeaderActionRegistry();
const { register: registerSubHeaderActions } = useSubHeaderActionRegistry();
onMounted(() => {
// Register in main header (top-right)
registerHeaderActions({
id: 'save-report',
label: 'Save Report',
color: 'primary',
onClick: () => saveReport(),
permission: {
route: '/reports',
actions: ['create']
}
});
// Register in sub-header (page level)
registerSubHeaderActions({
id: 'filter-toggle',
label: 'Filters',
side: 'left',
onClick: () => toggleFilters()
});
});
</script>
Powerful Features
- Permission Integration: Every action automatically uses PermissionGate
- Route Awareness: Show/hide actions based on current page
- Custom Components: Inject complete custom widgets
- Reactive Properties: Dynamic labels, loading states, conditional visibility
- Positioning Control: Left/right positioning in sub-header
** Complete Header Actions Guide** - Full documentation with advanced examples
Fetching Data from API
<script setup>
// Using custom API wrapper (recommended)
const { data: usersData, pending, error, refresh } = await useApi('/user_definition', {
query: {
limit: 10,
fields: 'id,email,name,role.name',
include: 'role'
},
key: 'users-list'
});
// Or use the standard useApi() composable for any custom call
const { data: directData, execute: loadDirect } = useApi('/user_definition', {
query: { limit: 10 },
});
await loadDirect();
// Computed for easy access
const users = computed(() => usersData.value?.data || []);
// Manual refresh
const loadUsers = () => {
refresh();
};
// Handle API responses
watch(error, (newError) => {
if (newError) {
toast.add({
title: 'Error loading users',
description: newError.message,
color: 'error'
});
}
});
onMounted(() => {
console.log('Users loaded:', users.value.length);
});
</script>
<template>
<div>
<!-- Loading state -->
<div v-if="pending">Loading users...</div>
<!-- Error state -->
<UAlert v-else-if="error" color="red">
{{ error.message }}
</UAlert>
<!-- Data display -->
<div v-else>
<div v-for="user in users" :key="user.id">
{{ user.name }} - {{ user.role?.name }}
</div>
<UButton @click="loadUsers">Refresh</UButton>
</div>
</div>
</template>
See API Integration for complete API usage guide.
Creating Forms
Extensions can use Enfyra's powerful form system to create dynamic, validated forms:
** Complete Form System Guide** - Learn about dynamic forms, validation, and field types
<template>
<UCard>
<FormEditor
:schema="schema"
v-model="formData"
@submit="handleSubmit"
/>
</UCard>
</template>
<script setup>
const schema = await useSchema('products');
const formData = ref(schema.generateEmptyForm());
const handleSubmit = async () => {
const { isValid, errors } = schema.validate(formData.value);
if (!isValid) {
toast.add({
title: 'Validation failed',
color: 'red'
});
return;
}
await useApi('/products', {
method: 'POST',
body: formData.value
});
};
</script>
Widget System
Using Widget Extensions
Widgets are reusable components that can be embedded anywhere:
<template>
<div class="grid grid-cols-2 gap-4">
<!-- Embed widget by database ID (not extensionId) -->
<Widget :id="5" />
<!-- Another widget -->
<Widget :id="6" />
</div>
</template>
Important: Widget id is the numeric database ID from the Extensions list, not the extensionId string.
Creating Widget Extensions
- Create Extension with type "Widget":
- Name: Widget display name
- Type: Select "Widget"
- Description: What this widget does
-
No menu linking needed (widgets are embedded, not navigated to)
-
Write Widget Code:
<template>
<UCard>
<template #header>
<h3>Sales Summary</h3>
</template>
<div class="text-2xl font-bold">
${{ totalSales.toLocaleString() }}
</div>
<p class="text-gray-500">This month</p>
</UCard>
</template>
<script setup>
// All functions are available globally - use directly
const totalSales = ref(0);
// Load data using custom wrapper
onMounted(async () => {
const { data, error } = await useApi('/sales_summary');
if (!error.value && data.value) {
totalSales.value = data.value.total;
}
});
</script>
- Embed Widget: Use
<Widget :id="database_id" />in any extension or page
Global Extension System
Global extensions are Vue SFC records with type="global". eApp fetches every enabled global extension during layout initialization, resolves it through the normal dynamic extension loader, and mounts it invisibly at shell level. Use this for logic that must exist across every page.
When to Use Global Extensions
Use a global extension for: - Account panel items such as a notification bell - Global unread counters or status indicators - Shared admin Socket.IO listeners - Background refresh bridges that update app-wide state - Shell-level registry entries that should survive route changes
Do not use a global extension for: - Full page content - Route-specific UI - Large dashboards or operational pages - Widgets that should be embedded in one page only - Floating cards or custom overlays that duplicate shell UI
Creating a Global Extension
Create an extension_definition record with:
- Type: global
- Menu: empty
- Template: empty or hidden
- Script: registry and realtime setup
Global extensions should register visible UI into existing shell registries. They should not render page body UI directly.
<template></template>
<script setup>
const unread = ref(3)
const iconName = (name) => ['lucide', name].join(':')
const activeBellIcon = iconName('bell-ring')
const idleBellIcon = iconName('bell')
const chevronIcon = iconName('chevron-right')
const NotificationPanelItem = defineComponent({
name: 'NotificationPanelItem',
setup() {
const openNotifications = () => navigateTo('/notifications')
return () => h('button', {
type: 'button',
class: 'flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left transition hover:bg-muted',
onClick: openNotifications,
}, [
h('span', {
class: 'flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary',
}, [
h(UIcon, {
name: unread.value > 0 ? activeBellIcon : idleBellIcon,
class: 'h-5 w-5',
}),
]),
h('span', { class: 'min-w-0 flex-1' }, [
h('span', { class: 'block truncate text-sm font-medium text-highlighted' }, 'Notifications'),
h('span', { class: 'mt-0.5 block truncate text-xs text-muted' }, unread.value > 0 ? 'Needs review' : 'All caught up'),
]),
unread.value > 0
? h(UBadge, { color: 'primary', variant: 'soft', size: 'sm' }, () => String(unread.value))
: h(UIcon, { name: chevronIcon, class: 'h-4 w-4 text-muted' }),
])
},
})
const { register } = useAccountPanelRegistry()
register({
id: 'notifications',
order: 20,
component: NotificationPanelItem,
})
const { adminSocket } = useAdminSocket()
const handleSummary = (payload) => {
if (payload?.unread != null) unread.value = payload.unread
}
adminSocket.on('notification:summary', handleSummary)
onUnmounted(() => {
adminSocket.off('notification:summary', handleSummary)
})
</script>
Global Extension UI Rules
- Keep account-panel items as one compact row: icon, label, short secondary text, trailing badge or chevron.
- Use shell-compatible tokens/classes such as
bg-muted,text-muted, andtext-highlighted. - Use
rounded-lgor smaller radii and moderate padding so the row matches the sidebar panel. - Make the entire row one
buttonwithtype="button". - Do not nest buttons inside account-panel rows.
- Do not render page-scale cards, modal shells, or hero-style UI from a global extension.
- Use stable registry ids so reloads replace the same shell item predictably.
- Clean up socket and DOM listeners in
onUnmounted; eApp unmounts old global components when extensions reload or are disabled.
File Upload Support
Extensions can handle file uploads:
<template>
<input
type="file"
@change="handleFileUpload"
accept=".vue"
/>
</template>
<script setup>
const handleFileUpload = async (event) => {
const file = event.target.files[0];
if (!file) return;
const content = await file.text();
// Process the file content
console.log('File content:', content);
};
</script>
Using NPM Packages in Extensions
Extensions can use npm packages to add powerful functionality like charts, utilities, and data processing.
Quick Start
1. Install Package - Go to Packages in the sidebar - Click Install Package - Select App Package type - Search and install your package
2. Use in Extension
<script setup>
onMounted(async () => {
const { dayjs, lodash } = await getPackages();
const date = dayjs().format('YYYY-MM-DD');
const total = lodash.sum([1, 2, 3]);
console.log('Date:', date, 'Total:', total);
});
</script>
Usage Patterns
Destructuring (Recommended)
const { chartjs, dayjs } = await getPackages();
Array of Packages
const packages = await getPackages(['chartjs', 'dayjs']);
All Packages
const allPackages = await getPackages();
Complete Example: Date Utilities
<template>
<UCard>
<template #header>
<h3>Date Utilities</h3>
</template>
<div class="space-y-4">
<div>
<UButton @click="formatDate">Format Current Date</UButton>
</div>
<div v-if="formattedDate">
<UBadge>{{ formattedDate }}</UBadge>
</div>
<div>
<UButton @click="addDays" variant="outline">Add 7 Days</UButton>
</div>
<div v-if="futureDate">
<UBadge color="green">{{ futureDate }}</UBadge>
</div>
</div>
</UCard>
</template>
<script setup>
const formattedDate = ref(null);
const futureDate = ref(null);
const formatDate = async () => {
const { dayjs } = await getPackages();
formattedDate.value = dayjs().format('YYYY-MM-DD HH:mm:ss');
};
const addDays = async () => {
const { dayjs } = await getPackages();
futureDate.value = dayjs().add(7, 'day').format('YYYY-MM-DD');
};
</script>
Complete Example: Chart with Data
<template>
<UCard>
<template #header>
<div class="flex items-center justify-between">
<h3>Revenue Chart</h3>
<UButton @click="loadData" :loading="loading" size="sm">
Refresh
</UButton>
</div>
</template>
<canvas ref="chartCanvas"></canvas>
</UCard>
</template>
<script setup>
const chartCanvas = ref(null);
const loading = ref(false);
let chartInstance = null;
const loadData = async () => {
loading.value = true;
const { Chart } = await getPackages(['chart.js']);
const ctx = chartCanvas.value.getContext('2d');
if (chartInstance) {
chartInstance.destroy();
}
chartInstance = new Chart(ctx, {
type: 'line',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
datasets: [{
label: 'Revenue',
data: [12000, 19000, 15000, 25000, 22000, 30000],
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
plugins: {
legend: {
display: true
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
loading.value = false;
};
onMounted(() => {
loadData();
});
</script>
Available Packages
Any npm package can be installed. Popular choices:
Charts & Visualization
- chart.js - Charts and graphs
- echarts - Enterprise charts
- apexcharts - Modern charting
Utilities
- dayjs - Date manipulation
- lodash - Utility functions
- axios - HTTP requests
Data & Forms
- vuedraggable - Drag and drop
- sortablejs - Sortable lists
- papaparse - CSV parsing
For complete documentation, see Package Management
Extension Management
Enabling/Disabling Extensions
- Go to Settings > Extensions
- Find your extension in the list
- Toggle the switch to enable/disable
- Disabled extensions won't load even if menu is clicked
Editing Extensions
- Click on any extension card to open editor
- Modify the code in the editor
- Click "Save" to recompile
- Changes take effect immediately
Version Control
- Update version number when making changes
- System tracks creation and modification timestamps
- User information stored for audit trail
Best Practices
Code Organization
<template>
<!-- Keep template clean and organized -->
<div class="extension-container">
<ExtensionHeader />
<ExtensionContent />
<ExtensionFooter />
</div>
</template>
<script setup>
// 1. Imports and composables
// All composables are available globally - just call them directly
const toast = useToast();
// 2. Reactive state
const state = reactive({
loading: false,
data: []
});
// 3. Computed properties
const filteredData = computed(() => {
return state.data.filter(item => item.active);
});
// 4. Methods
const loadData = async () => {
// Implementation
};
// 5. Lifecycle hooks
onMounted(() => {
loadData();
});
</script>
<style scoped>
.extension-container {
@apply p-6;
}
</style>
Error Handling
<script setup>
const loadData = async () => {
// useApi handles errors internally - no try-catch needed
const { data, error } = await useApi('/endpoint');
if (error.value) {
console.error('Extension error:', error.value);
toast.add({
title: 'Error',
description: error.value.message,
color: 'red'
});
return;
}
// Handle success with data.value
console.log('Data loaded:', data.value);
};
</script>
Performance Tips
- Use
computedfor derived values - Implement pagination for large datasets
- Lazy load heavy components
- Clean up resources in
onUnmounted
Common Issues and Solutions
Extension Not Loading
Problem: Clicking menu shows blank page Solution: 1. Check extension is enabled 2. Verify extension is linked to correct menu 3. Check browser console for compilation errors 4. Ensure user has permission to access
Compilation Errors
Problem: Extension fails to compile Solution: 1. Check Vue.js syntax is correct 2. Ensure all imported components exist 3. Verify script setup syntax 4. Look for error messages in the form
Missing Components
Problem: Components not recognized
Solution:
- Components are auto-injected and available directly in template
- No need to import or access through props
- Just use them directly: <UButton>, <UCard>, etc.
API Calls Failing
Problem: Cannot fetch data Solution: 1. Check user has required permissions 2. Verify API endpoint exists 3. Check network tab for errors 4. Ensure proper authentication
Advanced Patterns
State Management
<script setup>
// Use useState for cross-component state
const globalState = useState('myExtension', () => ({
counter: 0,
items: []
}));
// Update state
globalState.value.counter++;
</script>
Component Composition
<script setup>
// Break large extensions into smaller components
const components = {
Header: {
template: '<div>Header</div>'
},
Footer: {
template: '<div>Footer</div>'
}
};
</script>
<template>
<component :is="components.Header" />
<component :is="components.Footer" />
</template>
Dynamic Loading
<script setup>
const dynamicComponent = ref(null);
const loadComponent = async () => {
// Load component based on conditions
if (someCondition) {
dynamicComponent.value = await loadExtension('widget-1');
}
};
</script>
<template>
<component :is="dynamicComponent" v-if="dynamicComponent" />
</template>
Security Considerations
Permission Checks
Always verify permissions in your extensions:
<script setup>
const { hasPermission } = usePermissions();
// Check before showing sensitive data
const canViewFinancials = computed(() => {
return hasPermission('/financial_reports', 'GET');
});
// Check before allowing actions
const deleteRecord = async (id) => {
if (!hasPermission('/records', 'DELETE')) {
toast.add({
title: 'Permission denied',
color: 'red'
});
return;
}
await useApi(`/records/${id}`, { method: 'DELETE' });
};
</script>
Input Validation
Validate user input before sending to API:
<script setup>
const validateEmail = (email) => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
};
const submitForm = async () => {
if (!validateEmail(formData.value.email)) {
toast.add({
title: 'Invalid email address',
color: 'red'
});
return;
}
// Proceed with submission
};
</script>
Summary
The Extension System provides a powerful way to add custom functionality to Enfyra:
1. Create Menu Defines the navigation entry
2. Create Extension Provides the page content
3. Link Together Menu and extension work as one
4. Write Vue Code Full Vue 3 SFC support with auto-injected components
5. Full SDK Access Complete access to all Enfyra SDK features and composables
6. Access Resources UI components, API, permissions, Vue functions
7. Deploy Instantly No build process required
Extensions give you the flexibility to create any custom functionality while maintaining the security and consistency of the Enfyra platform. With full SDK integration, extensions have the same power as the core application.