ActiveAdminのカスタマイズ集 - 実用的で頻出な記法をコードサンプルにまとめてみた

## はじめに

ActiveAdminの使用を続ける中でカスタマイズ時に頻繁に使用する記法が見えてきたので、それらを詰め込んだコードサンプルを掲載します。

## コードサンプルの特徴

- ActiveAdminで `app/admin/...` に配置する1ファイルとして作成した
- 私がよく書く記法を詰め込み、TIPSとしても読めそうな内容にした
- テンプレートファイルにも流用できる構成を目指した
- なるべく公式ドキュメントに沿ったが、一部に公式外の記法も混ぜた

## 想定する読者

- ActiveAdminを使ったことがある人
  - インストール方法については記載しません
- ActiveAdminの便利な記法やカスタマイズ方法を探している人

## バージョン情報

記事公開時点の最新バージョンを使用します。

- Ruby: `3.3.1`
- Ruby on Rails: `7.1.3`
- ActiveAdmin: `3.2.0`

## サンプルのテーブル構成

今回は図の中央にある `company_users` の管理画面を作成します。
has_many / belongs_toの両方を持っている点が特徴です。

![ER図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231426/cc22708f-0ef6-61c6-8c8c-0bba81dde637.png)

```rb
class CompanyUser
  belongs_to :company
  has_many :roles
  # 自動で提案されるRansack用のメソッドは省略
end
```

## サンプルファイル

```rb
# app/admin/company_users.rb
ActiveAdmin.register CompanyUser do
  # 1. ヘッダーメニューの並び順
  menu priority: 21

  # 2. 使用可能なアクション一覧
  actions :all, except: [:destroy]

  # 3. 編集可能なカラム名
  permit_params :company_id, :name, :active

  controller do
    # 4. DBからレコードを取得する際の処理を追加
    def scoped_collection
      super.includes(:company)
    end
  end

  # 5. フィルターをRansackの記法でカスタム
  filter :name
  filter :active
  filter :updated_at
  filter :company_id, as: :select, collection: -> { Company.all }
  filter :company_name_cont, label: '会社名 (部分一致)'

  # 6-1. 一覧表示の内容をカスタム
  index do
    selectable_column
    column :id
    column :company_id do |record|
      a href: admin_company_path(record.company) do
        "#{record.company.name}(#{record.company.code})"
      end
    end
    column :name
    column :active
    actions
  end

  # 6-2. 詳細表示の内容をカスタム
  show do
    attributes_table do
      row :id
      row :company_id do |record|
        a href: admin_company_path(record.company) do
          "#{record.company.name}(#{record.company.code})"
        end
      end
      row :name
      row :active
      row :created_at
      row :updated_at
    end
  end

  # 6-3. 編集フォームの内容をカスタム
  form do |f|
    semantic_errors(*object.errors.attribute_names)

    inputs do
      input :company_id, as: :select, collection: Company.all, include_blank: '未選択'
      input :name
      input :active
    end

    actions
  end
end
```

### 動作確認のために作成したリポジトリ

- [GitHubリポジトリ](https://github.com/ljourm/sample-activeadmin)
  - [実際に動作確認したファイル](https://github.com/ljourm/sample-activeadmin/blob/66e5fa867effbcda879ec4b1249383ab2c977af5/app/admin/company_user_qiita_sample.rb)

## 解説

### 1. ヘッダーメニューの並び順

```rb
menu priority: 21
```

[公式ドキュメント Customize the Menu](https://activeadmin.info/2-resource-customization.html#customize-the-menu)

`menu`を省略した場合の並び順は辞書順(アルファベット順)です。メニューには求めている並び順があると思いますので必ず設定した方がいいと思います。

「CompaniesとCompanyUsersの間にメニューを追加したい」というケースは結構ありますので、連番(1→2→3)にせず、飛び番(11→21→31)のように設定しておいて間に挿入できるようにしておくのが好みです。

![ヘッダーメニューの設定例](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231426/87b96f02-4276-7b47-6c31-ad2071c319a1.png)

### 2. 使用可能なアクション一覧

```rb
actions :all, except: [:update, :destroy]
```

[公式ドキュメント Disabling Actions on a Resource](https://activeadmin.info/2-resource-customization.html#disabling-actions-on-a-resource)

使用可能なアクションを変更する方法は「許可したいアクションを列挙(＝ホワイトリスト方式)」と「許可したくないアクションを列挙(＝ブラックリスト方式)」の2種類が用意されています。
公式ドキュメントには後者(ブラックリスト)しか見つけられませんでしたが、前者(ホワイトリスト)も動作しました。

```rb
# 設定可能な値
[:index, :new, :create, :edit, :update, :destroy]

# 閲覧のみ可能にする (ホワイトリスト方式の例)
actions :index, :show

# 削除を不可にする (ブラックリスト方式の例)
actions :all, except: [:destroy]
```

無効にしたアクションはリンクも自動で消えてくれるので結構便利です。

### 3. 編集可能なカラム名

```rb
permit_params :company_id, :name, :active
```

[公式ドキュメント Setting up Strong Parameters](https://activeadmin.info/2-resource-customization.html#setting-up-strong-parameters)

StrongParameter　(変更してよいパラメータを制御するためのRailsが提供している機能)　の設定です。これを設定しないとレコードを作成・更新することができなくなるので必ず設定します。

### 4. DBからレコードを取得する際の処理を追加

```rb
controller do
  def scoped_collection
    super.includes(:organization)
  end
end
```

[公式ドキュメント Customizing resource retrieval](https://activeadmin.info/2-resource-customization.html#customizing-resource-retrieval)

N+1問題の対策やwhere句の追加で活躍する書き方です。
以下にさらに複雑なサンプルを記載します。

```rb
controller do
  def scoped_collection
    records = super

    # active=trueのレコードだけを取得する。
    records = records.where(active: true)

    # N+1問題も対策する。
    records = records.includes(:company)

    # show画面の場合にのみrolesもN+1問題を対策する。
    if action_name == 'show'
      records = records.includes(:roles)
    end

    # 戻り値を設定する (忘れがちなので注意)
    records
  end
end
```

### 5. フィルターをRansackの記法でカスタム

```rb
filter :name
filter :active
filter :updated_at
filter :company_id, as: :select, collection: -> { Company.all }
filter :company_name_cont, label: '会社名 (部分一致)'
```

[公式ドキュメント Index Filters](https://activeadmin.info/3-index-pages.html#index-filters)

一覧画面の右側のフィルターをカスタマイズするための記法です。
基本的には`filter :カラム名`のように書くことが多いですが、セレクトやラジオを使ったり、関連レコードを条件に指定したりする方法も用意されています。

セレクトやラジオを使用する場合は `collection -> { Company.all }` のようにモデル一覧を渡すか、`collection: -> { Company.all.map { |company| [company.name, company.id] } }`のように`[表示名, id]`の配列を設定してください。

関連レコードを条件に指定する場合はRansackのドキュメントを読むと理解が深まると思います。
- [Ransack公式ドキュメント](https://activerecord-hackery.github.io/ransack/)
  - [Matcher一覧](https://activerecord-hackery.github.io/ransack/getting-started/search-matches/)

![フィルターのスクリーンショット](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231426/2731eb99-f284-8fc3-7708-9c56b4dbc49d.png)

### 6-1. 一覧表示の内容をカスタム
```rb
index do
  selectable_column
  column :id
  column :company_id do |record|
    a href: admin_company_path(record.company) do
      "#{record.company.name}(#{record.company.code})"
    end
  end
  column :name
  column :active
  actions
end
```

[公式ドキュメント Customizing the Index Page](https://activeadmin.info/3-index-pages.html)

`index do~end` を省略した場合、一覧画面には全カラムが表示されます。
表示内容をカスタマイズしたり、表示するカラムを絞ったりする場合にこれを記述します。

便利と思っている記法は`company_id`のところです。今回のサンプルでは会社名+会社コードを表示し、さらにリンクにしてみました。

![一覧表示の解説](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231426/0750c2b1-f8d7-b321-febb-3e01a86b9c82.png)

### 6-2. 詳細表示の内容をカスタム
```rb
show do
  attributes_table do
    row :id
    row :company_id do |record|
      a href: admin_company_path(record.company) do
        "#{record.company.name}(#{record.company.code})"
      end
    end
    row :name
    row :active
    row :created_at
    row :updated_at
  end
end
```

[公式ドキュメント Customize the Show Page](https://activeadmin.info/6-show-pages.html)
やっていることは一覧画面と同じです。

![詳細画面の解説](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231426/dc6db1d5-ae2b-fd55-0053-da4e2d435dcb.png)

サンプルには載せませんでしたが、panelやsidebarを活用することでたくさんの情報を体系的に表示することも可能です。

- [公式ドキュメント Panel](https://activeadmin.info/12-arbre-components.html#panels)
- [公式ドキュメント Sidebar Sections](https://activeadmin.info/7-sidebars.html)

### 6-3. 編集フォームの内容をカスタム

```rb
form do |f|
  semantic_errors(*object.errors.attribute_names)

  inputs do
    input :company_id, as: :select, collection: Company.all, include_blank: '未選択'
    input :name
    input :active
  end

  actions
end
```

[公式ドキュメント Forms](https://activeadmin.info/5-forms.html)

semantic_errors以外はこれまでに紹介した機能と概ね同じ記入です。
company_idのcollectionは[5. フィルターをRansackの記法でカスタム](#5-フィルターをransackの記法でカスタム)を参照してください。

`semantic_errors(*object.errors.attribute_names)`はバリデーションエラーが画面に表示されないケースの対策です。公式ドキュメントに書かれていませんが、便利なのでこれを毎回書くようにしています。
「保存ボタンを押したのに画面が動かない (＝保存に失敗したのにエラーメッセージが表示されないため、画面が動かないように見える)」が発生することがありました。関連レコードやフォームから変更できないカラムでバリデーションエラーが発生した場合にこれが発生します。個別に対応するのが正しいとは思いますが、とりあえずこの記法で全エラーを表示させておけば問題ないケースが多いため、いつもこれを記述するようにしています。
ちなみに`semantic_errors`のデフォルトの動作は:baseのみのバリデーションエラーを表示することです。
[Qiita参考記事](https://qiita.com/murata0705/items/6dccb5467233816808f7#%E7%B7%A8%E9%9B%86%E7%94%BB%E9%9D%A2%E3%81%AB%E3%82%A8%E3%83%A9%E3%83%BC%E3%83%A1%E3%83%83%E3%82%BB%E3%83%BC%E3%82%B8%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%95%E3%81%9B%E3%81%9F%E3%81%84)
[GitHub Issue](https://github.com/activeadmin/activeadmin/pull/905)

![スクリーンショット 2024-04-29 232140.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/231426/9e786801-a530-d227-ff43-f65975231755.png)

## おわりに

Ruby on Railsで「最速でデータを閲覧・変更できる管理画面が欲しい」と思った際、最初に思い浮かぶ候補はActiveAdminだと思います。
ActiveAdminに対しては「カスタマイズ性が悪い」とネガティブな意見を聞くこともあります。そんな時に対応策としてこの記事が活用できたらと考えて作成してみました。

実装速度に魅了されている私はメリット・デメリットを捉えながら今後もActiveAdminの使用を続けていくことになりそうです。
v4に向けた動き([v4.0.0.beta1のリリースノート](https://github.com/activeadmin/activeadmin/releases/tag/v4.0.0.beta1))もあるようですし、今後の動向も楽しみですね。