Rails7とInfoldを使って管理画面を素早く作る

# はじめに
Railsエンジニアの皆さん、管理画面作るときはどうしていますか？
ActiveAdminなどの管理画面系Gemを使うのも手ですが、

- DSLでカスタマイズするのが辛い
- 欲しい機能が実現できないことがある
- デザインがあまり好みでない

私個人はこんな理由で、既存のGemは使わず、イチから開発していました。しかし、都度イチから開発するのは非効率ですし、管理画面は共通化できる要素が多いことも特徴です。
そこで、自作フレームワークを作って開発を効率化してきました。この仕組みが私自身の開発を大きく改善できたので、是非みなさんにも共有したいと思い、Gemにして公開しました。

今回はこのGem [Infold](https://github.com/yamataka22/infold) の使い方をご紹介いたします。

https://github.com/yamataka22/infold

# どんなツール？
まずは [こちらのデモサイト](https://demo.infold.dev/admin) をご参照ください。
このアプリは、 **Gemから自動生成した機能だけで構築** しています。

## ツールの特徴
以下をテーマとして開発をしています。
1. コードを自動生成する
2. DSLは使わない
3. モダンで使い勝手の良いUI/UX

### 1. コードを自動生成する

といってもGithub Copilotのような代物ではなく、RailsのScaffoldを拡張しています。Scaffoldはリソースの定義をするだけで、controller, model, viewなどのファイルが自動で生成されてCRUDできる基本機能が自動で出来上がります。この仕組みを応用して、**管理画面に特化したジェネレータ**を開発しました。

また、生成されるコードは、可読性を重視しています。ただ動けばよいのではなく、生成されたコードをエンジニア自身が容易に可読でき、追加のカスタマイズがしやすい仕組みを目的としています。

### 2. DSLは使わない

設定はDSLではなくYAMLに記載します。このYAMLを上記のジェネレータに読み込ませると、設定に応じたCRUDアプリのコードを自動生成できます。

YAMLなのでDSLのようにロジックをゴリゴリ書くことはできませんが、シンプルでわかりやすいことを重視しています。全ての機能をGemとDSLで実現するのではなく、無理なく共通化できるレベルまでを自動生成し、追加のカスタマイズは通常のRails開発と同様にできたほうがよいと考えています（なので、コードを自動生成する仕組みを採用しています）

とはいえ、YAMLだけでもそれなりの機能性を実現しています。[デモサイト](https://demo.infold.dev/admin)でも実装していますが、例えば以下のような機能性があります。

- has_many なデータの一括登録・参照
- Validation, Enum, Decoratorなどの仕組み
- 画像ファイルの登録や、CSVファイルの出力、などなど

### 3. モダンでユーザビリティの高いUI/UX

Hotwireを利用して、SPAライクなUI/UXを実現しています。また、UIのテンプレートにはBootstrap5をベースにした [tabler](https://tabler.io/) を採用しています。これにより、モダンでユーザビリティの高い管理画面を実現しています。

管理画面の開発には、デザイナーの方は参画せず、エンジニアだけで画面のUIを含めて構築することが多いかと思います。画面周りが苦手なエンジニアの方だけでも、ユーザビリティに優れた管理画面を実現できることを目指しています。

## 対応するRailsのバージョン
- Hotwireでの動作を想定していることから、Railsは7.0以上で動作します
- また、Railsはesbuild等でJSのビルド環境が必要となります

# 実際の使い方
前置きが長くなりましたが、ここからはインストールから実際のカスタマイズまでをご説明します。

## Infoldのインストール
Gemfileに[infold]と、infoldで利用している各種Gemを追記し、 `bundle install` します。

```ruby:Gemfile
gem 'infold', require: false
gem 'devise'           # 認証
gem 'haml-rails'       # HAML
gem 'kaminari'         # ページネーション
gem 'view_component'   # View Component
gem 'active_decorator' # Decoration
gem 'enum_help'        # Enum Helpers
```

次に、infoldのジェネレータからインストールをします。

```bash
$ rails generate infold:install
```

このタイミングで、view_componentやstimulusなどで利用する共通的なファイルが生成されます。
また、Deviseのマイグレーションも生成されますので、テーブルの作成と、rails consoleからテスト用アカウントの登録をしておきます

```bash
$ rails db:migrate
$ rails console

AdminUser.create(email: 'user@example.com', password: 'password')
```
以上でインストールは完了です。このタイミングで `bin/dev` からRailsを起動後、ブラウザで `http://localhost:3000/admin` にアクセスすると、ログイン画面が表示されるので、先程作成したユーザー情報でログインします。
![01_installed.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172629/db0c1da7-ca66-dd10-135f-18fef3ade185.png)
これでログイン機能が作れました。続けて、実際にアプリを構築していきます。

## 顧客管理アプリの作成
まずは顧客（Customer）モデルを作成します。ここでは通常のRailsの`generate model`を使います。

```bash
$ bin/rails g model Customer name:string address:string gender:integer
$ bin/rails db:migrate
```

infoldのジェネレータを使って、このモデルに対するアプリを作成します。

```bash
$ bin/rails g infold Customer
```

これだけで顧客管理アプリが生成されています。この段階でブラウザから`localhost:3000/admin/customers` にアクセスすると、CUSTOMERSアプリが表示されます。
![04_customer_search.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172629/a0a50a0c-50ab-7401-e0f1-7bd4ac107328.png)
画面右上のオレンジ色のボタン「Create New」をクリックするとフォームが表示されます。適当に入力して画面右下の青色のボタン「Create」から登録ができます。
![03_customer_form.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172629/6007fe9f-2fe6-9f44-2d3c-6c26c55f6cee.png)
レコードの左端「ノートアイコン」から参照画面、右端「鉛筆アイコン」から編集画面が表示されます。
![05_customer_show.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172629/91db57ba-390f-d851-f99c-eaa639d05dc0.png)
ほんの数ステップで、CRUDをベースとした管理画面アプリが生成することができました。

このタイミングでControllerやViewなどのファイルが自動で生成されています。例えばControllerは `app/controllers/admin/customer_controller.rb` が以下の通り生成されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172629/519abb82-3ebf-56ff-3665-5b11561abe2b.png)
このように、**InfoldはActiveAdminのようにアプリそのものの生成ではなく、ソースコードを生成し、それを動作させる仕組み**になっています。また、コードは一般的なRailsで書かれているので、Railsエンジニアの方であれば編集が容易にできます。ツールだけで実現できないカスタマイズは、このコードを直接編集することで、柔軟な対応が可能になります。

### 顧客管理アプリのカスタマイズ
上記で生成したアプリは、YAMLで設定をすることでカスタマイズができます。試しに以下の機能を追加してみます。
- Nameを必須項目にする
- Genderは male, female, other のEnumとし、ラジオボタンから選択する

前述の `rails g infold Customer` のタイミングで、プロジェクトの `config/infold/customer.yml` が生成されています。このファイルを以下の内容に変更してください。

```yaml:config/infold/customer.yml
model:
  validate:
    name: presence
  enum:
    gender:
      male: 1
      female: 2
      other: 3
app:
  form:
    fields:
      - name
      - address
      - gender:
          kind: radio
```

YAMLの編集後、改めて `g infold` を実行します。
```bash
$ bin/rails g infold Customer
```

これで、YAMLの設定に応じたコードが再生成されます。ブラウザから動作確認をしてご確認ください。バリデーションとラジオボタンのフォームが有効化されます。
![07_customer_validate.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172629/3d8f2aff-5b26-8449-aab1-380ac1654ca7.png)
このように、InfoldではYAMLを編集してカスタマイズを行います。

## 商品管理アプリの作成
続けて、商品管理を作成してみます。

Railsのモデル生成
```bash
$ bin/rails g model Product title:string price:integer
$ bin/rails db:migrate
```

infoldでアプリの生成
```bash
$ bin/rails g infold Product
```

ブラウザで`localhost:3000/admin/products` にアクセスすると、商品管理アプリが表示されます。ヘッダーメニューも「PRODUCTS」が追加されています。
![08_products.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172629/b9fc8cdc-7a2d-1a49-4b49-fe8404b6f71a.png)

### 商品画像の表示
InfoldはActiveStorageをサポートします。そこで、例えば商品画像を管理したい場合、`config/infold/product.yml`を以下の設定にします。
```yaml:config/infold/product.yml
model:
  active_storage:
    photo:
      kind: image
app:
  index:
    list:
      fields:
        - id
        - title
        - photo
        - price
  form:
    fields:
      - title
      - photo:
          kind: file
      - price
```
:::note
なお、プロジェクトでActiveStorageがインストールされていることが前提となります。
```bash
$ bin/rails active_storage:install
$ bin/rails db:migrate
```
:::

infoldでアプリを生成します。
```bash
$ bin/rails g infold Product
```
上記設定で、フォームから画像が登録でき、一覧に表示されます。
![09_product_image_form.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172629/923a7d24-2f2b-89e5-664e-3ba597c8748d.png)
![10_product_images.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172629/444f30a6-9b18-2ee3-a169-897b5e942bc1.png)

## 注文管理アプリの作成
上記の「顧客」「商品」管理は、単一のテーブルをCRUDとしていましたが、次に作る「注文管理」は、顧客と商品のリレーションを設定していきます。
RailsからOrderモデルを作成します。
```bash
$ bin/rails g model Order customer:references product:references amount:integer
$ bin/rails db:migrate
```
以下のリレーションになります。
![er1_ja.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172629/71bc520c-c351-8c89-19c1-3ea74a5dd550.png)
注文管理アプリをinfoldから生成します。`g infold:resource` とすることで、YAMLファイルのみの生成ができます。
```bash
$ bin/rails g infold:resource Order
```
Orderと他モデルにアソシエーションを設定するために、`config/infold/order.yml` を以下のように編集します。
なお、以下の ! 部分にある `name_field` は、レコードの名称を表すフィールドを指定します。その結果、関連先のレコードをそのフィールドで表示できます。例えば、orderモデルは`customer_id`を介してcustomerモデルとの関連を持ちますが、画面上では `customer_id` を表示する代わりに、`customer.name` を表示します。
```yaml:config/infold/order.yml
model:
  association:
    customer:
      kind: belongs_to
      name_field: name # !
    product:
      kind: belongs_to
      name_field: title # !
app:
  form:
    fields:
      - customer:
          kind: select
      - product:
          kind: select
      - amount:
          kind: number
```
YAML編集後、Infoldのジェネレータでコードを生成します。
```bash
$ bin/rails g infold Order
```
ブラウザから確認すると、例えば登録フォームからは、CustomerやProductがリストから選択可能になっています。
![11_order_form.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172629/0004c5a3-207d-bcae-3874-a68152e9446f.png)
また、Orderを登録後、Customer, Productはリンクで表示され、それをクリックすると詳細を参照できるようになります。
![12_order_product.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172629/2272f94a-87b9-0b26-fbd1-0a2b8c685bf3.png)
# 応用編
## has_manyの一括登録
現状の注文管理は、OrderモデルがProductモデルに直接関連しているため（ `order belongs_to product`）、一度の注文に一つの商品しか購入できません。複数商品をまとめて購入できるよう、`OrderDetail` を追加し、以下のモデリングに変更します（ `order has_many order_details`, `order_detail belongs_to product` )。
![er2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172629/2ae4adff-25c0-ffb5-4642-35d01d930f7e.png)
Rails側でモデルの変更・追加をしていきます。まず、OrderとProductの関連を除去するために、以下のマイグレーションを生成します。
```bash
$ bin/rails g migration RemoveProductFromOrders product:references amount:integer
```
また、Orderモデルにも `belongs_to :product` の記述が残っているので、削除します。
```diff_ruby:models/order.rb
class Order < ApplicationRecord
  belongs_to :customer
- belongs_to :product
end
```
次に、OrderDetailモデルを作成し、マイグレーションを実行します。
```bash
$ bin/rails g model OrderDetail order:references product:references amount:integer
$ bin/rails db:migrate
```
Infoldで注文管理 (`config/infold/order.yml`) を変更していきます。
```diff_yaml:config/infold/order.yml
model:
  ...
  association:
    customer:
      kind: belongs_to
      name_field: name
-   # 以下は削除
-   product:
-     kind: belongs_to
-     name_field: title
+   # 以下を追加
+   order_details:
+     kind: has_many
+     dependent: destroy
+     model: # 関連先モデル（order_details）の設定も可能
+       validate:
+         product: presence
+       association: # order_detailsの更に関連先（product）
+         product:
+           kind: belongs_to
+           name_field: title
app:
  # 参照画面にも一括表示するために以下を追加
+ show:
+   fields:
+     - id
+     - customer
+     - order_details:
+         fields:
+           - product
+           - amount
  form:
    fields:
      - customer:
          kind: select
-     # 以下は削除
-     - product
-         kind: select
-      - amount
-          kind: number
+     # 以下を追加
+     - order_details:
+         kind: association
+         fields:
+           - product:
+               kind: select
+           - amount:
+               kind: number
```
この状態でInfoldで注文管理アプリを再生成します。
```bash
$ bin/rails g infold Order
```
フォームで一括登録が可能になります。[ADD]ボタンをクリックすると行が追加され、行の右端「ゴミ箱アイコン」をクリックで削除できます。
![13_order_details_form.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172629/ea5462fd-3979-9601-e8a8-0fb025f599dc.png)

## 子画面からの検索・指定
現状の注文管理(orders)の登録フォームでは、顧客(customer)がリストになっています。顧客データが多くなると、リストでは該当のレコードを指定することが困難になります（例えば顧客データが100件あったら、リストの選択肢も100件になり、使い物にならない）
Infoldでは、belongs_toによる関連モデルを、子画面から検索・指定できます。注文管理から顧客を子画面で指定するように変更してみます。

まず、顧客の子画面検索を有効化するために、顧客管理側のYAML (customer.yml) を編集していきます。
```diff_yaml:config/infold/customer.yml
model:
  ...
app:
  form:
    ...
+ # 以下を追加
+ association_search:
+   conditions:
+     - id:
+         sign: eq
+     - name:
+         sign: full_like
+   list:
+     fields:
+       - id
+       - name
```
次に、注文管理側のYAML (order.yml) を編集します（kindをselectからassociation_searchに変更）。
```diff_yaml:config/infold/order.yml
model:
  ...
app:
  form:
    fields:
      - customer:
-         kind: select
+         kind: association_search
      - order_details:
        ...         
```
顧客管理、注文管理、それぞれのコードをInfoldから再生成します。
```bash
$ bin/rails g infold Customer
$ bin/rails g infold Order
```
ブラウザで 注文管理の登録フォームを表示すると、顧客のフォームが変わっていることが確認できます。このフォームの青い虫眼鏡ボタンをクリックすると、子画面で顧客の検索画面が表示されます。
子画面上で検索をし、一覧の右端列「チェックアイコン」をクリックすると、該当の顧客が注文管理フォームに引き継がれます。
![14_association_search.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172629/527fcb16-1e60-d265-8037-e8895672d6bd.gif)

## 日本語化
Infoldはi18nに対応しています。日本語表示をする場合、Railsのdefault_localeを `ja` に変更します。詳細は[Railsガイド](https://railsguides.jp/i18n.html)などを参照ください。

一例として、`config/initializers/locale.rb` を以下の通り作成します。
```ruby:config/initializers/locale.rb
I18n.config.available_locales = [:ja, :en]
I18n.default_locale = :ja
I18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}').to_s]
```

また、[rails-i18n](https://github.com/svenfuchs/rails-i18n/blob/master/rails/locale/ja.yml) から ja.yml をダウンロードして、`config/locales`へ設置してください。

その後、`config/locales` ディレクトリに、`models`ディレクトリを作成し、その中に`customer.ja.yml`を以下の内容で作成します。
```yml:config/locales/models/customer.ja.yml
ja:
  activerecord:
    attributes:
      customer:
        name: 氏名
        address: 住所
        gender: 性別
```
顧客管理の画面が日本語化されます（ボタンやメッセージ内容も日本語化されています）
![15_locale1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172629/f544dce1-ac59-b742-16ad-db9196f2b513.png)

なお、Enumの区分値を日本語化する場合は、以下の通りです。
```diff_yaml:config/locales/models/customer.ja.yml
ja:
  activerecord:
    attributes:
      customer:
        name: 氏名
        address: 住所
        gender: 性別
+ enums:
+   admin: # adminを挟む必要があります
+     customer:
+       gender:
+         male: 男性
+         female: 女性
+         other: その他
```
Enumも含めて日本語化ができました。
![16_locale2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172629/02c0ad1f-e4fa-3d18-29f5-fd93be586177.png)

# まとめ
長くなりましたが、Infoldの一連の使い方をご説明いたしました。

YAMLの設定だけでも、それなりの機能を生成できることが確認できたかと思います。
あとは、自動生成後のソースコードをカスタマイズする流れになります。

イチから実装する場合と比較して、管理画面の実装スピードをかなり高速化できると思いますので、是非ご利用を検討頂けたら幸いです！

## 今回の説明で使ったコード
上記で生成したアプリは、Githubにて公開しています。

https://github.com/yamataka22/infold.demo_qiita