ActiveAdmin のメニュー設定をすごく便利にする ActiveAdmin::MenuTree のご紹介

ActiveAdminのメニュー設定を便利にする [ActiveAdmin::MenuTree](https://rubygems.org/gems/activeadmin-menu_tree) というgemを作ったのでご紹介します。


### ActiveAdminとは

https://activeadmin.info/

https://github.com/activeadmin/activeadmin

<!--
* [Active Admin | The administration framework for Ruby on Rails](https://activeadmin.info/)
* [activeadmin/activeadmin: The administration framework for Ruby on Rails applications.](https://github.com/activeadmin/activeadmin)
-->

ActiveAdminとは、Railsで管理画面をさくっと作れるフレームワークですね。とても便利です。
ここではActiveAdmin自体についてはある程度知っている(使ったことがある)前提として話を進めます。

なお、[ActiveAdmin v2.10.0](https://github.com/activeadmin/activeadmin/releases/tag/v2.10.0) 時点の情報になります。


### ActiveAdminのメニューはカスタマイズできる

さて、これをお読みいただいているみなさまは、ActiveAdminのメニューをカスタマイズする方法はご存知でしょうか。

ActiveAdminでは、リソースを作ればデフォルトで何もしなくてもメニューに表示されるので、小規模プロジェクトだと気にならずにそのまま使っている場合も多いかもしれません。

しかし、少し規模が大きくなってリソースの数が増えてくると、どこに何があるかわかりづらくなりますよね。

#### 何もしないとこんな感じになってしまう
![dummy before](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52311/b262e82b-c6c3-24d4-9724-9d58e6e94a77.png)
たくさんあるとたいへん

#### こういうふうにカスタマイズできます
![dummy after](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52311/e60719d1-f921-27cb-b0ef-f8c965c7e5c6.png)
グループ化したり、順番を入れ替えたり、好きなラベルを設定したりできます。

<!--
![dummy after](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52311/ad383cc0-ebf4-70dc-913a-f78f05bfa73a.png)
![dummy after](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52311/98084b6d-e160-4078-5875-5a3e3dce5269.png)
-->

このように、ActiveAdminではメニューをある程度カスタマイズする方法が提供されています。


### ActiveAdmin公式のメニューカスタマイズ方法

まずはActiveAdminでできるメニューカスタマイズの方法をご紹介します。

公式ドキュメントに書いてある内容をベースに紹介します。

https://activeadmin.info/2-resource-customization.html#customize-the-menu

#### ActiveAdminの各リソースでmenuメソッドを使う

まず、ActiveAdminの各リソースで`menu`メソッドに引数を渡すのが基本的な設定方法になります。

たとえば、menuメソッドに`false`を渡すことで特定のリソースでメニューを無効にでき、そのリソースがメニューに表示されなくなります。
もちろんページ自体はなくならないので、パスを開けばアクセスできます。

```ruby
# app/admin/post.rb
ActiveAdmin.register Post do
  menu false
end
```

<!-- このように、各ActiveAdminリソースで`menu`メソッドに引数を渡すことで、メニューをカスタマイズできます。 -->

#### menuメソッドで使える４つのオプション
`menu`メソッドでは以下の４つのオプションによってメニューをカスタマイズできます。

* `:label` - 文字列かprocでメニューに表示するラベルを設定できる
* `:parent` - 文字列IDかラベルで親メニューを指定できる(ネストさせることができる)
* `:if` - ブロックかメソッドのシンボルでメニューを表示するかどうか制御できる
* `:priority` - 数値で優先度を設定できる(デフォルトは10)


<!-- `label`や`if`の使い方はそのままなので省略して、 -->
ここでは`priority`と`parent`を中心に紹介します。

#### Menu Priority
* `prioriy`でメニューの優先度を設定できます
* 数値が小さいほど左側に表示されます
* 数値が同じ場合は名前順になります
* デフォルトは10

```ruby
ActiveAdmin.register Post do
  menu priority: 1
end
```

#### Drop Down Menus
* `parent`で親メニューを設定することで、メニューをグループ化できます
* 指定した`parent`(以下の例の`Blog`)は、存在しなければ自動で作られます

```ruby
ActiveAdmin.register Post do
  menu parent: "Blog"
end
```

* 配列で指定するとさらにネストできます

```ruby
ActiveAdmin.register Post do
  menu parent: ["Admin", "Blog"]
end
```

#### Customizing Parent Menu Items
* 親メニューをカスタマイズする場合はイニシャライザで設定します
* 通常の`menu`と同じオプションが使えます
* 子メニューからは`parent`で同じラベル名(またはid)を指定することで識別します

```ruby
# config/initializers/active_admin.rb
config.namespace :admin do |admin|
  admin.build_menu do |menu|
    menu.add label: "Blog", priority: 0
  end
end

# app/admin/post.rb
ActiveAdmin.register Post do
  menu parent: "Blog"
end
```

#### Dynamic Parent Menu Items
* 親メニューのラベルを動的にしたい場合は、`id`を設定すれば子メニューから参照できます

```ruby
# config/initializers/active_admin.rb
config.namespace :admin do |admin|
  admin.build_menu do |menu|
    menu.add id: "blog", label: proc{ "Something dynamic" }, priority: 0
  end
end

# app/admin/post.rb
ActiveAdmin.register Post do
  menu parent: "blog"
end
```


### ここが不便だよActiveAdmin

以上のように、ActiveAdminではメニューのカスタマイズが可能なのですが、主に以下の２点が少し不便だと思っています。

* `priority` での順序管理
  * 一度設定したメニューの順番を変更する場合、各リソースの`priority`を一つずつ変更する必要がある
  * adminリソースのファイルはメニューの順番に並んでるわけではないので、どれがどれだかわからなくなる
* `parent` での階層関係の管理
  * 親メニューをカスタマイズするにはイニシャライザで設定する必要がある

ひとことで言うと、設定が各リソースファイルに分散していて変更が面倒です。
特に親メニューと通常のメニューが混在して順序を管理したい場合などに顕著になると思います。


### そこで ActiveAdmin::MenuTree
ActiveAdmin::MenuTree を使えば、前述の不便な点を解消して、メニューをyamlで直感的に管理できます。

https://github.com/shuuuuun/activeadmin-menu_tree

https://rubygems.org/gems/activeadmin-menu_tree

<!--
* [shuuuuun/activeadmin-menu_tree: Allows ActiveAdmin menus to be managed in tree format.](https://github.com/shuuuuun/activeadmin-menu_tree)
* [activeadmin-menu_tree | RubyGems.org | コミュニティのGemホスティングサービス](https://rubygems.org/gems/activeadmin-menu_tree)
-->

### ActiveAdmin::MenuTree の使い方
ここからようやく ActiveAdmin::MenuTree の使い方の紹介になります。

#### インストール
インストールは普通にgemを追加するだけ。

```ruby
# Gemfile
gem 'activeadmin-menu_tree'
```
```sh
# インストール
$ bundle install
```

#### 使い方はたった２ステップ！
かんたんです。

##### 1. menu_tree の設定

yamlで設定を書いて、イニシャライザで読み込む。

```yaml
# config/activeadmin-menu_tree.yml or anywhere you like
activeadmin:
  menu_tree:
    - id: Dashboard
    - label: Admin
      children:
        - id: AdminUser
          label: Admin Users
        - id: Comment
          label: Admin Comments
```
```ruby
# config/initializers/activeadmin-menu_tree.rb
ActiveAdmin::MenuTree.setup do |config|
  config.menu_tree = YAML.load_file(Rails.root.join("config/activeadmin-menu_tree.yml"))["activeadmin"]["menu_tree"]
end
```

##### 2. ActiveAdminの各リソースで menu_tree を呼び出す

通常は`menu`メソッドで設定しているところを`menu_tree`に変更

```ruby
# app/admin/admin_users.rb
ActiveAdmin.register AdminUser do
  menu_tree
  # ...
end

# app/admin/dashboard.rb
ActiveAdmin.register_page "Dashboard" do
  menu_tree label: proc { I18n.t("active_admin.dashboard") }
  # ...
end
```

* `priority`/`parent`は設定不要になるので消してOK
* `menu`メソッドと同様のオプションを渡せるので、`label`や`if`はそのまま動的に設定可能

##### これでRailsを起動すればメニューがいい感じに設定されています

![sample](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52311/0ba8e2a7-1c72-f5be-473e-48bebba8b85b.png)


#### その他の設定読み込み方法

また、ActiveAdmin::MenuTreeでは、yamlを使わずにシンプルにHashで設定することもできます。
`proc`を使った動的な指定をしたい場合は、こちらを使うのがよいかもしれません。

```ruby
ActiveAdmin::MenuTree.setup do |config|
  config.menu_tree = [
    { id: "Dashboard", label: proc { I18n.t("active_admin.dashboard") } },
    {
      label: "Foo",
      if: proc { "Something dynamic" }
      children: [
        { id: "Bar" },
        { id: "Baz" }
      ]
    }
  ]
end
```

[Config gem](https://github.com/rubyconfig/config)を使っている場合は、`to_hash`メソッドでHashにすることで使えます。

```ruby
ActiveAdmin::MenuTree.setup do |config|
  config.menu_tree = Settings.activeadmin.menu_tree.map(&:to_hash)
end
```
```yaml
# config/settings.yml
activeadmin:
  menu_tree:
    # ...
```

[Global gem](https://github.com/railsware/global)などでも同様にHashに変換すれば使えると思います。


#### フル設定例

yamlで使える全設定例は以下のような感じです。

```yaml
activeadmin:
  menu_tree:
    # Specify the resource with `id`.
    - id: Dashboard
    - id: Product
    # Specify a menu label with `label`.
    - label: User Info
      # Specify child elements with `children`.
      children:
        - id: User
        - id: Profile
    - label: Admin
      children:
        - id: AdminUser
          label: Admin Users
        # Comment resource will be handled specially.
        - id: Comment
          label: Admin Comments
    - label: Others
      children:
        - id: Foo
        - id: Bar
    - label: Example Site
      # You can pass the other options available for `menu` DSL, like `url`, `html_options`.
      url: 'https://example.com'
      html_options:
        target: blank
    # Nesting of children is also available.
    - label: Lorem
      children:
        - label: ipsum
          children:
            - label: dolor
              children:
                - label: sit
                  children:
                    - label: amet
                      url: 'https://wikipedia.org/wiki/Lorem_ipsum'
                      html_options:
                        target: blank
```

* `id` - リソースIDを指定します(通常はモデル名)
* `label` - ラベルを指定します
* `children` - 子要素を配列で指定します
* `Comment`リソースは通常リソースファイルがないですが、特別に処理されます
* `url`, `html_options`など、`menu`メソッドで使えるオプションはそのまま渡せます
  * ※yamlなのでrubyの動的な指定はできません

<img width="1573" alt="screenshot" src="https://user-images.githubusercontent.com/7542105/153759374-fd516cb8-8022-4e44-aad9-c97f77afc4e7.png">

こんな感じで、 ActiveAdmin::MenuTree を使えば ActiveAdmin のメニューをわかりやすく設定できます。


### おまけ： gemの命名規則について

gemを作ったことのある方なら知っているかと思いますが、gemの名前を決める際の指針として、[RubyGems公式のガイドライン](https://guides.rubygems.org/name-your-gem/)があります。

ざっくりポイントは以下の３点です。
・複数単語をくっつける場合はアンダースコア
・階層が分かれる場合はハイフン
・大文字や他の記号は使わない

そして、 ActiveAdmin と ActiveAdmin::MenuTree の命名は以下のようになっています。

GEM NAME|REQUIRE STATEMENT|MAIN CLASS
-|-|-
`activeadmin`|`require 'activeadmin'` <br> or `require 'active_admin'`|`ActiveAdmin`
`activeadmin-menu_tree`|`require 'activeadmin/menu_tree'`|`ActiveAdmin::MenuTree`

これを見ると、[`activeadmin`](https://rubygems.org/gems/activeadmin)というgem名はガイドラインを守っていないことになりますね。
一応requireでは`activeadmin`でも`active_admin`でもどちらでも読み込めるようになっています。[ref.](https://github.com/activeadmin/activeadmin/blob/v2.10.0/lib/activeadmin.rb)

ちなみに、ActiveAdminに限らずこのガイドラインを守っていない有名gemもそれなりにありますね。
（Rails関連の`activexxx`系や`actionxxx`系を筆頭に）

ActiveAdmin::MenuTree では、`activeadmin`の部分は本家と同じにし、`menu_tree`の部分はガイドラインに従う形にしました。

今後gemを作る方の参考になれば。

<!--
実はActiveAdminのgemはこのガイドラインを守っていません。
ガイドラインに従えば`active_admin`にすべきだと思いますが、gem名は[`activeadmin`](https://rubygems.org/gems/activeadmin)になっています。
固有名詞として一単語ということであれば、クラス名は`ActiveAdmin`ではなく`Activeadmin`になるはず。

今回はactiveadminの部分は本家と同じ形にし、以下のようなgem名にしました。
`activeadmin-menu_tree`
ちなみにクラスとしては以下のようになります。
`ActiveAdmin::MenuTree`
-->

### おわりに
* ActiveAdminのメニュー設定を便利にする [ActiveAdmin::MenuTree](https://rubygems.org/gems/activeadmin-menu_tree) というgemのご紹介でした。ぜひみなさん使ってみてください！
* 不具合など見つけた方は[issue](https://github.com/shuuuuun/activeadmin-menu_tree/issues)などで優しく教えてください
* プルリクエストも歓迎です