ACTION Extensions
Overview
Section titled “Overview”ACTION extensions add interactive buttons to headers, toolbars, and context menus. They trigger workflows, open dialogs, or perform quick operations on entities.
Use Cases
Section titled “Use Cases”- Quick actions on entities (archive, export, duplicate)
- Trigger workflows and automations
- Open dialogs and modals
- External integrations (send email, create ticket)
- Batch operations on multiple items
Basic Example
Section titled “Basic Example”{ point: 'work-unit.header.actions', plugin: 'my-plugin', priority: 80, extension: { type: ExtensionType.ACTION, id: 'my-action', label: 'My Action', icon: IconBolt, onClick: async (context) => { await performAction(context.workUnit); } }}ACTION Extension Properties
Section titled “ACTION Extension Properties”Required Properties
Section titled “Required Properties”- type:
ExtensionType.ACTION - id: Unique identifier
- label: Button label text
- onClick:
(context: any) => Promise<void>- Click handler
Optional Properties
Section titled “Optional Properties”- icon: Icon component from @aether/ui/icons
- variant:
'primary' | 'secondary' | 'subtle' | 'danger' - tooltip: Tooltip text on hover
- isDisabled:
(context: any) => boolean- Disable condition - isLoading:
(context: any) => boolean- Show loading state - confirmMessage: Confirmation dialog text
Real-World Example: Timer Toggle
Section titled “Real-World Example: Timer Toggle”{ point: 'work-unit.header.actions', plugin: 'timer', priority: 80, match: { businessType: ['SERVICE', 'PROJECT'] }, extension: { type: ExtensionType.ACTION, id: 'timer-toggle', label: (context) => { const timers = getTimersFromStorage(); return timers[context.workUnitId]?.isRunning ? 'Stop Timer' : 'Start Timer'; }, icon: IconClock, onClick: async (context) => { const timers = getTimersFromStorage(); const currentTimer = timers[context.workUnitId];
if (currentTimer?.isRunning) { await stopTimer(context.workUnitId); } else { await startTimer(context.workUnitId, context.workUnit.name); }
window.dispatchEvent(new Event('storage')); }, variant: (context) => { const timers = getTimersFromStorage(); return timers[context.workUnitId]?.isRunning ? 'danger' : 'primary'; } }}Dynamic Properties
Section titled “Dynamic Properties”Properties can be functions that receive context:
{ type: ExtensionType.ACTION, id: 'conditional-action', label: (context) => context.isComplete ? 'Reopen' : 'Complete', icon: (context) => context.isComplete ? IconArrowBack : IconCheck, variant: (context) => context.isComplete ? 'secondary' : 'primary', isDisabled: (context) => !context.user?.canEdit, onClick: async (context) => { await toggleComplete(context.workUnitId); }}Confirmation Dialogs
Section titled “Confirmation Dialogs”Show confirmation before dangerous actions:
{ type: ExtensionType.ACTION, id: 'delete-action', label: 'Delete', icon: IconTrash, variant: 'danger', confirmMessage: 'Are you sure you want to delete this item? This cannot be undone.', onClick: async (context) => { await deleteItem(context.workUnitId); showNotification({ title: 'Item deleted', color: 'success', }); }}Batch Actions
Section titled “Batch Actions”Actions on multiple selected items:
{ point: 'work-unit.list.actions', plugin: 'bulk-operations', priority: 70, extension: { type: ExtensionType.ACTION, id: 'bulk-archive', label: 'Archive Selected', icon: IconArchive, onClick: async (context) => { const selectedIds = context.selectedWorkUnits.map(w => w.id);
await archiveBulk(selectedIds);
showNotification({ title: `Archived ${selectedIds.length} items`, color: 'success', }); }, isDisabled: (context) => context.selectedWorkUnits.length === 0 }}Integration Actions
Section titled “Integration Actions”Trigger external integrations:
{ type: ExtensionType.ACTION, id: 'send-to-slack', label: 'Share to Slack', icon: IconBrandSlack, onClick: async (context) => { const response = await fetch('/api/slack/share', { method: 'POST', body: JSON.stringify({ workUnitId: context.workUnitId, channel: '#general', }), });
if (response.ok) { showNotification({ title: 'Shared to Slack', color: 'success', }); } }}Best Practices
Section titled “Best Practices”1. Provide Feedback
Section titled “1. Provide Feedback”onClick: async (context) => { try { await performAction(context.workUnitId); showNotification({ title: 'Action completed', color: 'success', }); } catch (error) { showNotification({ title: 'Action failed', message: error.message, color: 'error', }); }}2. Handle Loading States
Section titled “2. Handle Loading States”{ type: ExtensionType.ACTION, id: 'async-action', label: 'Process', isLoading: (context) => context.isProcessing, onClick: async (context) => { context.setIsProcessing(true); try { await longRunningOperation(); } finally { context.setIsProcessing(false); } }}3. Validate Before Acting
Section titled “3. Validate Before Acting”onClick: async (context) => { if (!context.workUnit.canBeArchived) { showNotification({ title: 'Cannot archive', message: 'Complete all tasks first', color: 'warning', }); return; }
await archive(context.workUnitId);}