Admin UI
Custom actions

Custom Actions

Custom actions allow you to add buttons and functionality to entity pages beyond standard CRUD operations.

Overview

Actions can be added to:

  • Index page: Bulk operations or entity-wide actions
  • Show page: Item-specific operations
  • Edit page: Additional save options or workflows
  • Menu: Global actions (see Menu configuration)

Action Configuration

type ActionConfig = {
  [key: string]: {
    label: string | ((params: ActionParams) => string);
    confirm?: string;
    newWindow?: boolean;
    action: string | ((params: ActionParams) => Promise<ActionResult>);
  }
}
 
type ActionParams = {
  entityUI: EntityUI;
  item?: any;
  id?: string;
  principal: Principal;
  runtime: ActiveQLServer;
  activeAdmin: ActiveAdmin;
}
 
type ActionResult = {
  message?: string;
  redirect?: string;
}

Index Actions

Actions on the index (list) page:

{
  entities: {
    Product: {
      index: {
        actions: {
          export: {
            label: 'Export to CSV',
            action: async ({ entityUI, runtime }) => {
              const products = await runtime.entity('Product').findAll();
              const csv = await generateCSV(products);
              const url = await saveFile(csv, 'products.csv');
              return { redirect: url };
            }
          },
          import: {
            label: 'Import Products',
            action: '/admin/products/import'
          },
          publishAll: {
            label: 'Publish All Draft',
            confirm: 'Publish all draft products?',
            action: async ({ runtime }) => {
              const count = await runtime.entity('Product').updateMany(
                { status: 'draft' },
                { status: 'published' }
              );
              return { message: `${count} products published` };
            }
          }
        }
      }
    }
  }
}

Show Actions

Actions on the detail page:

{
  entities: {
    Order: {
      show: {
        actions: {
          printInvoice: {
            label: 'Print Invoice',
            action: async ({ item }) => {
              const pdf = await generateInvoice(item);
              return { redirect: pdf.url };
            },
            newWindow: true
          },
          sendConfirmation: {
            label: 'Resend Confirmation',
            confirm: 'Send order confirmation email?',
            action: async ({ item, runtime }) => {
              await sendOrderConfirmation(item);
              return { message: 'Confirmation email sent' };
            }
          },
          cancel: {
            label: 'Cancel Order',
            confirm: 'This will cancel the order and refund the customer. Continue?',
            action: async ({ item, runtime }) => {
              await cancelOrder(item.id, runtime);
              return { message: 'Order cancelled and refunded' };
            }
          },
          duplicate: {
            label: 'Duplicate',
            action: async ({ item, entityUI }) => {
              const newItem = { ...item, id: undefined, status: 'draft' };
              const saved = await entityUI.entity.save(newItem);
              return { redirect: entityUI.editPath(saved.id) };
            }
          }
        }
      }
    }
  }
}

Edit Actions

Actions on the edit page:

{
  entities: {
    Article: {
      edit: {
        actions: {
          saveAndPublish: {
            label: 'Save & Publish',
            action: async ({ item, entityUI }) => {
              item.status = 'published';
              item.publishedAt = new Date();
              const saved = await entityUI.entity.save(item);
              return { redirect: entityUI.showPath(saved.id) };
            }
          },
          saveDraft: {
            label: 'Save as Draft',
            action: async ({ item, entityUI }) => {
              item.status = 'draft';
              const saved = await entityUI.entity.save(item);
              return { redirect: entityUI.editPath(saved.id) };
            }
          },
          preview: {
            label: 'Preview',
            action: ({ item }) => `/preview/article/${item.id}`,
            newWindow: true
          }
        }
      }
    }
  }
}

Action Types

String Action (Redirect)

Simple navigation:

{
  viewOnSite: {
    label: 'View on Website',
    action: '/products/${item.slug}',
    newWindow: true
  }
}

Function Action

Custom logic:

{
  archive: {
    label: 'Archive',
    confirm: 'Archive this item?',
    action: async ({ item, entityUI }) => {
      item.status = 'archived';
      item.archivedAt = new Date();
      await entityUI.entity.save(item);
      return { message: 'Item archived' };
    }
  }
}

Action Results

Show Message

Display a notification:

{
  action: async ({ item }) => {
    await processItem(item);
    return { message: 'Processing complete' };
  }
}

Redirect

Navigate to another page:

{
  action: async ({ item, entityUI }) => {
    const newItem = await duplicateItem(item);
    return { redirect: entityUI.showPath(newItem.id) };
  }
}

File Download

Return a download URL:

{
  action: async ({ item }) => {
    const pdf = await generatePDF(item);
    return { redirect: pdf.downloadUrl };
  }
}

Confirmation Dialogs

Add confirm to show a confirmation dialog:

{
  delete: {
    label: 'Delete Permanently',
    confirm: 'This action cannot be undone. Delete this item?',
    action: async ({ item, entityUI }) => {
      await entityUI.entity.delete(item.id);
      return { redirect: entityUI.indexPath() };
    }
  }
}

Dynamic Labels

Labels can be functions:

{
  toggle: {
    label: ({ item }) => item.isActive ? 'Deactivate' : 'Activate',
    action: async ({ item, entityUI }) => {
      item.isActive = !item.isActive;
      await entityUI.entity.save(item);
      return { message: `Item ${item.isActive ? 'activated' : 'deactivated'}` };
    }
  }
}

Permission Checks

Check permissions within actions:

{
  approve: {
    label: 'Approve',
    action: async ({ item, principal, entityUI }) => {
      if (!principal.roles.includes('approver')) {
        return { message: 'You do not have permission to approve' };
      }
 
      item.status = 'approved';
      item.approvedBy = principal.id;
      item.approvedAt = new Date();
 
      await entityUI.entity.save(item);
      return { message: 'Item approved' };
    }
  }
}

State Transitions

Use actions for state machine transitions:

{
  entities: {
    Order: {
      show: {
        actions: {
          confirm: {
            label: 'Confirm Order',
            action: async ({ item, entityUI }) => {
              if (item.status !== 'pending') {
                return { message: 'Order is not pending' };
              }
              item.status = 'confirmed';
              await entityUI.entity.save(item);
              return { message: 'Order confirmed' };
            }
          },
          ship: {
            label: 'Mark as Shipped',
            action: async ({ item, entityUI }) => {
              if (item.status !== 'confirmed') {
                return { message: 'Order must be confirmed first' };
              }
              item.status = 'shipped';
              item.shippedAt = new Date();
              await entityUI.entity.save(item);
              return { message: 'Order marked as shipped' };
            }
          },
          deliver: {
            label: 'Mark as Delivered',
            action: async ({ item, entityUI }) => {
              if (item.status !== 'shipped') {
                return { message: 'Order must be shipped first' };
              }
              item.status = 'delivered';
              item.deliveredAt = new Date();
              await entityUI.entity.save(item);
              return { message: 'Order delivered' };
            }
          }
        }
      }
    }
  }
}

Complete Example

{
  entities: {
    Product: {
      index: {
        actions: {
          export: {
            label: 'Export CSV',
            action: async ({ runtime }) => {
              const products = await runtime.entity('Product').findAll();
              const csv = generateCSV(products);
              return { redirect: `/download/products.csv?data=${csv}` };
            }
          },
          bulkPublish: {
            label: 'Publish All Draft',
            confirm: 'Publish all products in draft status?',
            action: async ({ runtime }) => {
              const result = await runtime.entity('Product').updateMany(
                { status: 'draft' },
                { status: 'published', publishedAt: new Date() }
              );
              return { message: `${result.count} products published` };
            }
          }
        }
      },
 
      show: {
        actions: {
          duplicate: {
            label: 'Duplicate',
            action: async ({ item, entityUI }) => {
              const copy = {
                ...item,
                id: undefined,
                name: `${item.name} (Copy)`,
                sku: `${item.sku}-COPY`,
                status: 'draft'
              };
              const saved = await entityUI.entity.save(copy);
              return { redirect: entityUI.editPath(saved.id) };
            }
          },
          publish: {
            label: ({ item }) => item.status === 'published' ? 'Unpublish' : 'Publish',
            confirm: ({ item }) => item.status === 'published'
              ? 'Remove this product from the website?'
              : 'Publish this product to the website?',
            action: async ({ item, entityUI }) => {
              if (item.status === 'published') {
                item.status = 'draft';
                item.publishedAt = null;
              } else {
                item.status = 'published';
                item.publishedAt = new Date();
              }
              await entityUI.entity.save(item);
              return {
                message: `Product ${item.status === 'published' ? 'published' : 'unpublished'}`
              };
            }
          },
          viewOnSite: {
            label: 'View on Website',
            action: ({ item }) => `/products/${item.slug}`,
            newWindow: true
          }
        }
      },
 
      edit: {
        actions: {
          saveAndNew: {
            label: 'Save & Create Another',
            action: async ({ item, entityUI }) => {
              await entityUI.entity.save(item);
              return { redirect: entityUI.newPath() };
            }
          },
          preview: {
            label: 'Preview',
            action: ({ item }) => `/preview/products/${item.id}`,
            newWindow: true
          }
        }
      }
    }
  }
}