Back to skills
extension
Category: Development & EngineeringNo API key required

plutonium-definition-actions

Add custom actions and interactions to Plutonium resources

personAuthor: jakexiaohubgithub

Definition Actions

Actions define custom operations that can be performed on resources. They can be simple (navigation) or interactive (with business logic via Interactions).

Action Types

| Type | Shows In | Use Case | |------|----------|----------| | resource_action | Index page | Import, Export, Create | | record_action | Show page | Edit, Delete, Archive | | collection_record_action | Table rows | Quick actions per row | | bulk_action | Selected records | Bulk operations |

Simple Actions (Navigation)

Simple actions link to existing routes. The target route must already exist - these don't create new functionality, just navigation links.

class PostDefinition < ResourceDefinition
  # Link to external URL
  action :documentation,
    label: "Documentation",
    route_options: {url: "https://docs.example.com"},
    icon: Phlex::TablerIcons::Book,
    resource_action: true

  # Link to custom controller action (you must add the action + route yourself)
  action :reports,
    route_options: {action: :reports},
    icon: Phlex::TablerIcons::ChartBar,
    resource_action: true
end

Important: When adding custom routes for actions, always use the as: option to name them:

# In your portal routes or config/routes.rb
resources :posts do
  collection do
    get :reports, as: :reports  # Named route required!
  end
end

This ensures resource_url_for can generate correct URLs, especially for nested resources.

Note: For custom operations with business logic, use Interactive Actions with an Interaction class instead. That's the recommended approach for most custom actions.

Interactive Actions (with Interaction)

class PostDefinition < ResourceDefinition
  action :publish,
    interaction: PublishInteraction,
    icon: Phlex::TablerIcons::Send

  action :archive,
    interaction: ArchiveInteraction,
    color: :danger,
    category: :danger,
    position: 1000,
    confirmation: "Are you sure?"
end

Action Options

action :name,
  # Display
  label: "Custom Label",           # Button text (default: name.titleize)
  description: "What it does",     # Tooltip/description
  icon: Phlex::TablerIcons::Star,  # Icon component
  color: :danger,                  # :primary, :secondary, :danger

  # Visibility
  resource_action: true,           # Show on index page
  record_action: true,             # Show on show page
  collection_record_action: true,  # Show in table rows
  bulk_action: true,               # For selected records

  # Grouping
  category: :primary,              # :primary, :secondary, :danger
  position: 50,                    # Order (lower = first)

  # Behavior
  confirmation: "Are you sure?",   # Confirmation dialog
  turbo_frame: "_top",             # Turbo frame target
  route_options: {action: :foo}    # Route configuration

Creating an Interaction

Basic Structure

# app/interactions/resource_interaction.rb (generated during install)
class ResourceInteraction < Plutonium::Resource::Interaction
end

# app/interactions/archive_interaction.rb
class ArchiveInteraction < ResourceInteraction
  presents label: "Archive",
           icon: Phlex::TablerIcons::Archive,
           description: "Archive this record"

  attribute :resource  # The record being acted on

  def execute
    resource.archived!
    succeed(resource).with_message("Record archived successfully.")
  rescue ActiveRecord::RecordInvalid => e
    failed(e.record.errors)
  rescue => error
    failed("Archive failed. Please try again.")
  end
end

With Additional Inputs

# app/interactions/company/invite_user_interaction.rb
class Company::InviteUserInteraction < Plutonium::Resource::Interaction
  presents label: "Invite User", icon: Phlex::TablerIcons::Mail

  attribute :resource  # The company
  attribute :email
  attribute :role

  # Configure form inputs
  input :email, as: :email, hint: "User's email address"
  input :role, as: :select, choices: %w[admin member viewer]

  # Validations
  validates :email, presence: true, format: {with: URI::MailTo::EMAIL_REGEXP}
  validates :role, presence: true, inclusion: {in: %w[admin member viewer]}

  def execute
    UserInvite.create!(
      company: resource,
      email: email,
      role: role,
      invited_by: current_user
    )
    succeed(resource).with_message("Invitation sent to #{email}.")
  rescue ActiveRecord::RecordInvalid => e
    failed(e.record.errors)
  end
end

Bulk Action (Multiple Records)

Bulk actions operate on multiple selected records at once. When a definition has bulk actions, the resource table automatically shows:

  • Selection checkboxes in each row
  • Bulk actions toolbar that appears when records are selected
# 1. Create the interaction (note: plural `resources` attribute)
class BulkArchiveInteraction < Plutonium::Resource::Interaction
  presents label: "Archive Selected", icon: Phlex::TablerIcons::Archive

  attribute :resources  # Array of records (note: plural)

  def execute
    count = 0
    resources.each do |record|
      record.archived!
      count += 1
    end
    succeed(resources).with_message("#{count} records archived.")
  rescue => error
    failed("Bulk archive failed: #{error.message}")
  end
end

# 2. Register the action in the definition
class PostDefinition < ResourceDefinition
  action :bulk_archive, interaction: BulkArchiveInteraction
  # bulk_action: true is automatically inferred from `resources` attribute
end

# 3. Add policy method
class PostPolicy < ResourcePolicy
  def bulk_archive?
    create?  # Or whatever permission level is appropriate
  end
end

Authorization for bulk actions:

  • Policy method (e.g., bulk_archive?) is checked per record - the backend fails the entire request if any selected record is not authorized
  • Records are fetched via current_authorized_scope - only records the user can access are included
  • The UI only shows action buttons that all selected records support (intersection of allowed actions)

Resource Action (No Record)

class ImportInteraction < Plutonium::Resource::Interaction
  presents label: "Import CSV", icon: Phlex::TablerIcons::Upload

  # No :resource or :resources attribute = resource action
  attribute :file

  input :file, as: :file

  validates :file, presence: true

  def execute
    # Import logic...
    succeed(nil).with_message("Import completed.")
  end
end

Interaction Responses

def execute
  # Success with message (redirects to resource automatically)
  succeed(resource).with_message("Done!")

  # Success with custom redirect (only if different from default)
  succeed(resource)
    .with_redirect_response(custom_dashboard_path)
    .with_message("Redirecting...")

  # Failure with field errors
  failed(resource.errors)

  # Failure with custom message
  failed("Something went wrong")

  # Failure with specific field
  failed("Invalid value", :email)
end

Note: Redirect is automatic on success. Only use with_redirect_response for a different destination.

Interaction Context

Inside an interaction:

  • current_user - The authenticated user
  • view_context - Access to helpers and view methods
def execute
  resource.update!(
    archived_by: current_user,
    archived_at: Time.current
  )
  succeed(resource)
end

Defining in Definition

Basic

class PostDefinition < ResourceDefinition
  action :publish, interaction: PublishInteraction
  action :archive, interaction: ArchiveInteraction
end

With Overrides

class PostDefinition < ResourceDefinition
  action :archive,
    interaction: ArchiveInteraction,
    collection_record_action: false,  # Don't show in table
    color: :danger,
    position: 1000                    # Show last
end

Inherited Actions

Actions defined in ResourceDefinition (created during install) are inherited by all definitions:

# app/definitions/resource_definition.rb (created during install)
class ResourceDefinition < Plutonium::Resource::Definition
  action :archive, interaction: ArchiveInteraction, color: :danger, position: 1000
end

# All definitions inherit the archive action automatically
class PostDefinition < ResourceDefinition
end

Portal-Specific Actions

Override actions for a specific portal:

# packages/admin_portal/app/definitions/admin_portal/post_definition.rb
class AdminPortal::PostDefinition < ::PostDefinition
  # Add admin-only actions
  action :feature, interaction: FeaturePostInteraction
  action :bulk_publish, interaction: BulkPublishInteraction
end

Default CRUD Actions

Plutonium provides these by default:

action :new,     resource_action: true,           position: 10
action :show,    collection_record_action: true,  position: 10
action :edit,    record_action: true,             position: 20
action :destroy, record_action: true,             position: 100, category: :danger

Authorization

Actions are authorized via policies:

# app/policies/post_policy.rb
class PostPolicy < ResourcePolicy
  def publish?
    user.admin? || record.author == user
  end

  def archive?
    user.admin?
  end
end

The action only appears if the policy method returns true.

Immediate vs Form Actions

Immediate - Executes without showing a form (when interaction has no extra inputs):

class ArchiveInteraction < Plutonium::Resource::Interaction
  attribute :resource  # Only resource, no other inputs
  # No input declarations

  def execute
    resource.archived!
    succeed(resource)
  end
end

Form - Shows a form first (when interaction has additional inputs):

class InviteUserInteraction < Plutonium::Resource::Interaction
  attribute :resource
  attribute :email
  attribute :role

  input :email
  input :role, as: :select, choices: %w[admin member]
  # Has inputs = shows form first
end

Common Patterns

Archive/Restore

class ArchiveInteraction < Plutonium::Resource::Interaction
  presents label: "Archive", icon: Phlex::TablerIcons::Archive
  attribute :resource

  def execute
    resource.archived!
    succeed(resource).with_message("Archived.")
  end
end

class RestoreInteraction < Plutonium::Resource::Interaction
  presents label: "Restore", icon: Phlex::TablerIcons::Refresh
  attribute :resource

  def execute
    resource.active!
    succeed(resource).with_message("Restored.")
  end
end

Send Notification

class SendReminderInteraction < Plutonium::Resource::Interaction
  presents label: "Send Reminder", icon: Phlex::TablerIcons::Bell
  attribute :resource
  attribute :message

  input :message, as: :text, hint: "Custom message (optional)"

  def execute
    ReminderMailer.with(record: resource, message: message).deliver_later
    succeed(resource).with_message("Reminder sent.")
  end
end

Related Skills

  • plutonium-definition - Overview and structure
  • plutonium-definition-fields - Fields, inputs, displays
  • plutonium-definition-query - Search, filters, scopes
  • plutonium-interaction - Writing interaction classes
  • plutonium-policy - Controlling action access