Actions

Uchi Actions allow your users to perform custom tasks on your records. For example you could add an action that bans a user, marks a received comment as spam, or resends a welcome email.

Actions are rendered in the header of the index, show, and edit pages. Depending on the view, they either apply to a single record (e.g. on the show and edit pages) or to the repository as a whole (e.g. on the index page, where there’s no specific record yet).

Using the default actions

Every repository comes with three actions out of the box, so you don’t have to write any code to get basic CRUD functionality:

  • Uchi::Action::New — links to the page for creating a new record. Visible on the :index view.
  • Uchi::Action::Edit — links to the page for editing a record. Visible on the :show view.
  • Uchi::Action::Delete — destroys a record, after asking for confirmation. Visible on the :edit view.

These are returned by the default #actions implementation on Uchi::Repository:

def actions
  [
    Action::New.new,
    Action::Edit.new,
    Action::Delete.new
  ]
end

Since #actions is a regular method, you can override it in your own repository to remove one of the defaults, reorder them, or combine them with your own actions (see Registering actions for a repository below).

Where an action appears

Each action has an #on configuration that determines which views it’s visible on:

Uchi::Action::Delete.new.on # => [Uchi::View::EDIT]

Custom actions are only visible on the :show view by default:

Uchi::Actions::SendWelcomeEmail.new.on # => [Uchi::View::SHOW]

You can change this on a per-instance basis when registering the action:

module Uchi
  module Repositories
    class Comment < Repository
      def actions
        [
          Uchi::Action::New.new,
          Uchi::Action::Edit.new,
          Uchi::Action::Delete.new.on(:show, :edit)
        ]
      end
    end
  end
end

Only put record-bound actions like Delete on record views (:show and :edit) — not :index. The index page’s header isn’t rendered for a specific record (it’s rendered with record: nil), and Delete needs a record to build its destroy link, so adding it to :index raises while rendering the page.

Where actions render

Actions render in the header of the index, show, and edit pages. The first #max_number_of_actions_outside_dropdown actions (2 by default) render directly as standalone buttons; any remaining actions are tucked away in a dropdown menu instead, so the header doesn’t get crowded as you register more actions.

To change how many actions are shown before the rest collapse into the dropdown, override #max_number_of_actions_outside_dropdown on your repository:

module Uchi
  module Repositories
    class Comment < Repository
      def max_number_of_actions_outside_dropdown
        1
      end
    end
  end
end

Registering actions for a repository

Each repository has an #actions method that returns an Array of actions available to records in that repository. To attach an action to a repository, return an instance of it from #actions:

module Uchi
  module Repositories
    class Office < Repository
      def actions
        [
          Uchi::Actions::SendWelcomeEmail.new
        ]
      end
    end
  end
end

Overriding #actions replaces the default New/Edit/Delete actions entirely. If you want to keep them and add your own, call super and append to it:

module Uchi
  module Repositories
    class Office < Repository
      def actions
        super + [Uchi::Actions::SendWelcomeEmail.new]
      end
    end
  end
end

Creating your own actions

Actions are classes that inherit from Uchi::Action and define a #perform method. For example, to create a simple action that sends a welcome email, add a file in app/uchi/actions/send_welcome_email.rb:

module Uchi
  module Actions
    class SendWelcomeEmail < Uchi::Action
      def perform(records, input = {})
        records.each do |record|
          UserMailer.welcome_email(record).deliver_later
        end

        Uchi::ActionResponse.success("Welcome email sent")
      end
    end
  end
end

#perform receives the records the action was triggered for. It should return a Uchi::ActionResponse; any other return value is treated as Uchi::ActionResponse.success.

Responses

The default response after performing an action is to redirect back to the page where the action was performed, falling back to the index page if the referrer isn’t available. For redirecting responses, the response message is shown as a flash message; download and Turbo Stream responses don’t show it. To customize the behavior you can return an explicit Uchi::ActionResponse from the action:

if things_went_well?
  Uchi::ActionResponse.success("Done")
else
  Uchi::ActionResponse.error("Nope")
end

If you want to redirect the user somewhere else, chain a redirect_to onto the response:

Uchi::ActionResponse.success("Done").redirect_to(path: "/some/other/url")

You can also trigger a file download:

Uchi::ActionResponse.success("Exported").download(file_path: path, filename: "export.csv")

or render a Turbo Stream response:

Uchi::ActionResponse.success.turbo_stream { turbo_stream.remove(record) }

The repository and context

While an action is being rendered or performed, it has access to:

  • #repository — the Uchi::Repository instance it’s currently being used with.
  • #context — the Uchi::Context the current request is running in.

Use these to build URLs, look up translations, or otherwise tailor the action to the repository it belongs to:

def perform(records, input = {})
  Uchi::ActionResponse.success.redirect_to(path: repository.routes.path_for(:index))
end

Naming and styling

By default, an action’s display name is looked up from uchi.action.[action_key].name, where action_key is the underscored, full class name with / replaced by .. For Uchi::Actions::SendWelcomeEmail the key is uchi.action.uchi.actions.send_welcome_email.name:

en:
  uchi:
    action:
      uchi:
        actions:
          send_welcome_email:
            name: "Send welcome email"

If no translation is found, the name falls back to a titleized version of the class name without its namespace (e.g. SendWelcomeEmail becomes “Send Welcome Email”). Override #name to customize it:

def name
  "Send welcome email"
end

The built-in actions don’t use this lookup, since their labels are repository-specific and use the repository’s own translations instead. New overrides #name to return repository.translate.link_to_new (e.g. “New author”). Edit and Delete label their buttons with repository.translate.link_to_edit(record) and repository.translate.link_to_destroy(record), which can include the record’s title (e.g. “Delete Jane Austen”). See Translations for how to customize those.

Use #style to control the action’s visual style (e.g. :default or :danger):

def style
  :danger
end

Customizing how an action renders

By default, actions render as a button that submits a POST request to execute the action (calling #perform). Some actions instead need to render as a plain link — Uchi::Action::Edit and Uchi::Action::New, for instance, link straight to a page instead of executing anything. To customize this, override:

  • #render_as_dropdown_item(record:, view:) — how the action renders as an item inside the actions dropdown menu.
  • #render_as_button(record:, view:) — how the action renders as a standalone button/link, for when it’s rendered outside the dropdown menu (see Where actions render).
module Uchi
  module Actions
    class ViewOnSite < Uchi::Action
      def render_as_dropdown_item(record:, view:)
        view.link_to(name, record.public_url, class: "block p-2")
      end

      def render_as_button(record:, view:)
        view.link_to(name, record.public_url, class: Uchi::Flowbite::Button.classes(style: style))
      end
    end
  end
end