Skip to content

ACTION Extensions

ACTION extensions add interactive buttons to headers, toolbars, and context menus. They trigger workflows, open dialogs, or perform quick operations on entities.

  • 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
{
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);
}
}
}
  • type: ExtensionType.ACTION
  • id: Unique identifier
  • label: Button label text
  • onClick: (context: any) => Promise<void> - Click handler
  • 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
{
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';
}
}
}

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);
}
}

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',
});
}
}

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
}
}

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',
});
}
}
}
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',
});
}
}
{
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);
}
}
}
onClick: async (context) => {
if (!context.workUnit.canBeArchived) {
showNotification({
title: 'Cannot archive',
message: 'Complete all tasks first',
color: 'warning',
});
return;
}
await archive(context.workUnitId);
}