The project is in a healthy, maintained state
Rails Engine that provides shared Administrate dashboards, helpers, fields, partials, and assets.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 3.13

Runtime

>= 0.19, < 0.21
>= 7.0, < 7.2
 Project Readme

yummy-guide-generic-administrate

yummy-guide-generic-administrate は、Yummy Guide 系 Rails アプリで共通利用する Administrate 拡張をまとめた Rails Engine です。

Administrate 本体を置き換えるものではなく、ダッシュボードの共通既定値、 フィルター UI、一覧表示 helper、共通 field などを再利用しやすくするための補助 gem です。

前提

  • Ruby >= 3.2.2
  • Rails >= 7.0, < 7.2
  • Administrate >= 0.19, < 0.21
  • sprockets-rails

インストール

Gemfile に追加します。

gem "yummy-guide-generic-administrate"

その後、依存 gem をインストールします。

bundle install

提供するもの

  • YummyGuide::Administrate::ApplicationDashboard
    • 一覧画面の既定ソートと固定列数の共通基底クラス
  • YummyGuide::Administrate::DefaultSorting
    • dashboard 側の既定ソート設定を controller に反映する concern
  • YummyGuide::Administrate::DatetimeFilterParameters
    • 日付、時、分の分割パラメータを 1 つの datetime 文字列へ正規化する concern
  • YummyGuide::Administrate::CollectionHelper
    • 一覧テーブルの固定列数、リンク生成、action partial 解決を補助する helper
  • YummyGuide::Administrate::FilterFormHelper
    • datetime フィルターや checkbox group の組み立てを補助する helper
  • YummyGuide::Administrate::FilterControlsHelper
    • dashboard の Field 型フィルター定義から Filter ボタンとモーダルフォームを描画する helper
  • YummyGuide::Administrate::TooltipHelper
    • 管理画面内で補足説明 tooltip を描画する helper
  • YummyGuide::Administrate::DatetimeInputHelper
    • 管理画面フォーム用の date + time 入力 helper
  • YummyGuide::Administrate::NumberInputHelper
    • 管理画面フォーム用の number input text 化 helper
  • 共通 partial / assets
    • collection partial
    • filter form partial
    • clipboards.js
    • datetime_input.js
    • fixed_submit_actions.js
    • filter_controls.js
    • filter_form.js
    • sticky_left_columns.js
    • tooltips.js
    • components.css
  • 共通 field
    • YummyGuide::Administrate::Fields::JsonPrettyField
    • YummyGuide::Administrate::Fields::VersionItemField
    • YummyGuide::Administrate::Fields::VersionWhodunnitField
    • YummyGuide::Administrate::Fields::Area::PictureField

利用方法

Dashboard の基底クラス

共通の既定ソートと固定列数設定を使う場合は、dashboard を YummyGuide::Administrate::ApplicationDashboard から継承します。

class ApplicationDashboard < YummyGuide::Administrate::ApplicationDashboard
end

必要に応じて各 dashboard 側で上書きできます。

class Admin::ArticleDashboard < ApplicationDashboard
  COLLECTION_ATTRIBUTES = %i[id title status created_at].freeze
  COLLECTION_SORTABLE_ATTRIBUTES = %i[id title status created_at].freeze
  INDEX_FIXED_COLUMNS_COUNT = 2

  def default_sorting_attribute
    :published_at
  end

  def default_sorting_direction
    :desc
  end
end

Controller concern / helper

管理画面の基底 controller で共通 concern と helper を読み込みます。

class Admin::ApplicationController < Administrate::ApplicationController
  include YummyGuide::Administrate::DefaultSorting
  include YummyGuide::Administrate::DatetimeFilterParameters

  helper YummyGuide::Administrate::CollectionHelper
  helper YummyGuide::Administrate::DatetimeInputHelper
  helper YummyGuide::Administrate::FilterControlsHelper
  helper YummyGuide::Administrate::FilterFormHelper
  helper YummyGuide::Administrate::NumberInputHelper
  helper YummyGuide::Administrate::TooltipHelper
end

NumberInputHelperAdministrate::ApplicationController の view helper として 自動適用されます。Administrate::ApplicationController を継承しない独自 admin controller で同じ挙動が必要な場合だけ、上記のように明示的に読み込んでください。 TooltipHelperAdministrate::ApplicationController の view helper として 自動適用されます。

Collection partial

ホストアプリ側の app/views/administrate/application/_collection.html.erb から、 engine の共通 partial に委譲します。

<%= render "yummy_guide/administrate/administrate/application/collection",
           collection_presenter: collection_presenter,
           page: page,
           resources: resources,
           table_title: table_title,
           namespace: namespace,
           resource_class: resource_class,
           collection_field_name: collection_field_name %>

固定ヘッダーの設定

1. 最小構成

gem 付属の collection partial をそのまま使う場合、table wrapper と table 本体に 必要な data-* 属性はすでに入っています。data-css-sticky-table は sticky 対象の 識別子として使い、wrapper 自体には縦横スクロールを持たせません。そのため、ページ スクロールに合わせて thead th が sticky で固定されます。カラム幅調整を使う場合は yummy_guide_administrate/column_resizer.js も読み込んでください。

内部的には以下のような構造になります。

<% table_definition = yummy_guide_administrate_collection_table_definition(
     page: page,
     collection_presenter: collection_presenter
   ) %>

<div class="scroll-table" data-css-sticky-table>
  <table
    aria-labelledby="<%= table_title %>"
    data-fixed-columns-count="<%= table_definition.fixed_columns_count %>"
    data-mobile-fixed-columns-count="<%= table_definition.mobile_fixed_columns_count %>"
    <% if table_definition.table_style.present? %>
      style="<%= table_definition.table_style %>"
    <% end %>
  >
    ...
  </table>
</div>

複数画面で幅設定を共有したい場合は、render local に column_width_storage_scope: "admin.reservations" のような任意の scope 名を渡してください。

2. 自前の table partial を使う場合

独自の collection partial を書く場合は、少なくとも以下を満たしてください。

  • table wrapper に data-css-sticky-table を付ける
  • data-css-sticky-table を付けた wrapper に縦横スクロールを持たせない
  • table に data-fixed-columns-count を付ける
  • table に data-mobile-fixed-columns-count を付ける
  • 固定列数・列幅・固定列class/styleは yummy_guide_administrate_collection_table_definition から参照する
  • header の aria-labelledby がページタイトルと対応している
<% table_definition = yummy_guide_administrate_collection_table_definition(
     page: page,
     collection_presenter: collection_presenter,
     column_names: %i[id name]
   ) %>

<div class="scroll-table" data-css-sticky-table>
  <table
    aria-labelledby="page-title"
    data-fixed-columns-count="<%= table_definition.fixed_columns_count %>"
    data-mobile-fixed-columns-count="<%= table_definition.mobile_fixed_columns_count %>"
    <% if table_definition.table_style.present? %>
      style="<%= table_definition.table_style %>"
    <% end %>
  >
    <thead>
      <tr>
        <% id_column = table_definition.sticky_column(:id) %>
        <th class="<%= id_column[:class] %>" style="<%= id_column[:style] %>">ID</th>
        <% name_column = table_definition.sticky_column(:name) %>
        <th class="<%= name_column[:class] %>" style="<%= name_column[:style] %>">Name</th>
        <th class="sticky actions-column">Actions</th>
      </tr>
    </thead>
    <tbody>
      <% resources.each do |resource| %>
        <tr>
          <td><%= resource.id %></td>
          <td><%= resource.name %></td>
          <td class="sticky actions-column">
            <%= link_to "Show", [:admin, resource] %>
          </td>
        </tr>
      <% end %>
    </tbody>
  </table>
</div>

sticky actions-column を action 列に付けると、右端列も固定できます。 左端の固定列数は dashboard 側の INDEX_FIXED_COLUMNS_COUNT で制御します。

Datetime filter

filter form の枠と datetime 入力 partial を組み合わせて利用できます。

<%= render "yummy_guide/administrate/filter_forms/frame",
           path: path,
           form: form,
           method: method,
           current_values: search_options do |f, values| %>
  <tr>
    <td><%= f.label :start_at, "開始日時" %></td>
    <td>
      <%= render "yummy_guide/administrate/filter_forms/datetime_field",
                 form_scope: form,
                 field_name: :start_at,
                 current_value: values["start_at"],
                 css_class: "#{form}_start_at" %>
    </td>
  </tr>
<% end %>

Datetime input helper

通常フォームで日付と時刻を分けた入力 UI を使う場合は、DatetimeInputHelper を読み込んで admin_datetime_* helper を使います。既存の LMJ 互換として、 helper 名は admin_ prefix のまま提供しています。

<%= admin_datetime_field_tag(
      "coupon[expiration_date]",
      @coupon.expiration_date,
      required: true,
      default_current_time: @coupon.new_record?
    ) %>

<%= admin_split_datetime_field_tag(
      date_name: "coupon[expiration_date_date]",
      hour_name: "coupon[expiration_date_hour]",
      minute_name: "coupon[expiration_date_minute]",
      value: @coupon.expiration_date
    ) %>

<%= admin_date_and_time_field_tag(
      date_name: "reservation_task[due_date_on]",
      time_name: "reservation_task[due_time]",
      value: @reservation_task.due_at
    ) %>

<%= admin_time_field_tag("option[valid_from_time]", @option.valid_from_time) %>

datetime_input.js は visible な date / time 入力と hidden の送信値を同期し、 不正な日付や時刻の submit を抑止します。

controller 側では、必要な filter key を指定して datetime パラメータを正規化します。

def search_term
  resource_params = params[resource_name] || params[resource_name.to_sym] || {}
  raw_filters = resource_params[:search] || resource_params["search"] || {}

  normalized_filters = normalize_datetime_filter_params(
    raw_filters,
    keys: %i[start_at end_at]
  )

  normalized_filters
end

Dashboard driven filter controls

dashboard に FILTER_ATTRIBUTES を定義すると、admin_filter_controls で既存の Administrate index header に Filter ボタンとモーダルフォームを描画できます。

Filter の設定方法

  1. dashboard に送信先と filter 対象項目を定義します。
class AdspendDashboard < ApplicationDashboard
  FILTER_PATH = ->(view, _locals) { view.admin_adspends_path }
  FILTER_CLEAR_PATH = ->(view, _locals) { view.admin_adspends_path }

  FILTER_ATTRIBUTES = {
    campaign_name: YummyGuide::Administrate::Filters::Text.with_options(label: "Campaign name"),
    yearmonth: YummyGuide::Administrate::Filters::Text.with_options(
      label: "Year month",
      inputmode: "numeric",
      pattern: "\\d{6}",
      placeholder: "YYYYMM"
    )
  }.freeze
end
  1. index header などで admin_filter_controls を呼び出します。page から dashboard を 解決し、FILTER_ATTRIBUTES が存在する場合だけ Filter ボタンとフォームを描画します。
<%= admin_filter_controls(
      page: page,
      search_options: search_options
    ) %>

送信先は helper の path: / clear_path: で明示することもできます。helper の指定は dashboard の FILTER_PATH / FILTER_CLEAR_PATH より優先されます。

<%= admin_filter_controls(
      page: page,
      path: admin_adspends_path,
      clear_path: admin_adspends_path,
      search_options: search_options,
      filter_locals: { show_discarded_filter: false },
      root_hidden_fields: { "reservation[order]" => current_order[:order] },
      extra_actions: [button_tag("Download PDF", type: "button", class: "button")]
    ) %>

標準 Field は Text, Select, Checkbox, RadioGroup, BooleanRadioGroup, CheckboxGroup, DateRange, DatetimeRange, DatetimeLocalRange, Custom です。 主な option は次のとおりです。

  • 共通: label, default, if, class, id, placeholder, inputmode, pattern
  • 選択系: collection または options, select_options
  • checkbox: checked_value, unchecked_value
  • boolean radio group: unspecified_label, true_label, false_label
  • checkbox group: group
  • range: from, to, from_default, to_default, css_class

label, collection, default, if などの option には Proc も指定できます。Proc は arity に応じて call, call(view_context), call(view_context, filter_locals) のいずれかで評価されます。

FILTER_ATTRIBUTES = {
  owner_id: YummyGuide::Administrate::Filters::Text.with_options(
    label: "Owner ID",
    inputmode: "numeric",
    if: ->(view, _locals) { !view.current_user&.owner? }
  ),
  status: YummyGuide::Administrate::Filters::Select.with_options(
    label: "Status",
    collection: ->(_view, locals) { locals[:status_collection] },
    select_options: { include_blank: true }
  ),
  visible: YummyGuide::Administrate::Filters::BooleanRadioGroup.with_options(
    label: "Visible",
    unspecified_label: "-",
    true_label: "Visible",
    false_label: "Hidden"
  )
}.freeze

カスタム Filter の作成方法

既存の Field 型で表現できない場合は、partial を使う方法と独自クラスを定義する方法があります。

partial だけで足りる場合は Custom を使います。partial には form, form_scope, field, current_values, filter_locals が渡されます。

FILTER_ATTRIBUTES = {
  price_range: YummyGuide::Administrate::Filters::Custom.with_options(
    partial: "admin/filters/price_range"
  )
}.freeze
<tr>
  <td><%= form.label field.name, field.label_text(self, filter_locals) %></td>
  <td>
    <%= form.number_field :min_price, value: current_values["min_price"] %>
    <%= form.number_field :max_price, value: current_values["max_price"] %>
  </td>
  <%= filter_field_clear_cell if respond_to?(:filter_field_clear_cell) %>
</tr>

filter_form.js を読み込んでいる場合、data-behavior="filter-field-clear" を持つ button は同じ tr 内の input / select / textarea / checkbox / radio / datetime filter だけをクリアします。

複数画面で再利用する filter 型は YummyGuide::Administrate::Filters::Base を継承して作ります。 単一 input なら input を実装し、行全体を制御したい場合は row または input_cell を上書きします。

class CurrencyFilter < YummyGuide::Administrate::Filters::Base
  private

  def input(view_context, form, current_values, locals)
    form.select(
      name,
      view_context.options_for_select(options.fetch(:collection), current_value(current_values)),
      { include_blank: true },
      html_options(view_context, locals)
    )
  end
end

FILTER_ATTRIBUTES = {
  payout_currency: CurrencyFilter.with_options(
    label: "Currency",
    collection: [["JPY", "JPY"], ["USD", "USD"]]
  )
}.freeze

Number input helper

Admin/Administrate 画面では、number_field / number_field_tagtype="text" かつ inputmode="decimal" の input として描画します。これにより、 ブラウザ標準の number spinner や mouse wheel による意図しない数値変更を避けます。

class, id, name, value, data, required, disabled, readonly, placeholder などの通常 option は維持されます。min, max, step, in, withintype="number" 前提のブラウザ制御なので出力しません。

range_field / range_field_tag は対象外で、従来どおり type="range" として描画 されます。raw HTML の <input type="number"> は helper を通らないため対象外です。

Tooltip helper

ラベルやボタンの横に補足説明用の tooltip アイコンを表示する場合は、 admin_tooltip を使います。PC では hover / focus 時に表示し、モバイルではタップで 表示・非表示を切り替えます。

<%= f.label :published_at %>
<%= admin_tooltip("公開日時を過ぎると公開ページに表示されます。") %>

説明本文に改行などの HTML を含める場合は block を渡します。text 引数と block を 同時に指定した場合は block が優先されます。

<%= admin_tooltip do %>
  Add - 調整レコードを追加表示するようにします。<br>
  Only - 調整レコードのみを表示します。
<% end %>

block を使わない場合、tooltip 本文はテキストとして扱われ、HTML は描画しません。 見た目と表示制御には components.csstooltips.js の読み込みが必要です。

Asset の読み込み

この engine の asset はホストアプリ側で明示的に読み込んでください。

//= require yummy_guide_administrate/clipboards
//= require yummy_guide_administrate/datetime_input
//= require yummy_guide_administrate/fixed_submit_actions
//= require yummy_guide_administrate/filter_controls
//= require yummy_guide_administrate/filter_form
//= require yummy_guide_administrate/sticky_left_columns
//= require yummy_guide_administrate/tooltips
 *= require yummy_guide_administrate/components

固定更新ボタン

管理画面の new / edit 系フォームでは、submit セクションを画面下に固定表示できます。 この機能は fixed_submit_actions.jscomponents.css に含まれる style によって動作します。

固定対象を明示指定する場合

固定したい submit セクションに data-fixed-submit-actions="true" を付けます。

<div class="form-actions" data-fixed-submit-actions="true">
  <%= f.submit %>
</div>

この指定がある場合、gem はその submit セクションを最優先で固定対象にします。 1 ページに複数フォームがあっても、明示指定した submit セクションだけが固定表示されます。

設定しなかった場合の挙動

data-fixed-submit-actions="true" が 1 件もない場合、gem は admin の new / edit ページで submit セクションを自動選択します。

  • 各フォーム内の .form-actions / .form_submit を候補にする
  • form-actions--top が付いた上部 submit は除外する
  • data-fixed-submit-exclude="true" が付いた submit セクションも除外する
  • 各フォームでは最後の submit セクションだけを候補にする
  • 複数フォームがあるページでは、現在表示中のフォームに対応する submit を固定する

上部 submit と下部 submit の両方があるフォームでは、下部 submit が自動で選ばれます。

fixed 帯に表示されるボタン

fixed 帯には submit セクションのクローンを表示します。

  • 元のフォーム内ボタンの見た目は変更しない
  • fixed 帯にだけ大きいボタンサイズを適用する
  • 1 つの submit セクションに複数 submit ボタンがある場合は全部表示する

自動選択から除外したい場合

自動選択の候補から外したい submit セクションには、data-fixed-submit-exclude="true" を付けます。

<div class="form-actions" data-fixed-submit-exclude="true">
  <%= f.submit "Preview" %>
</div>

Custom field

dashboard から共通 field を利用できます。

ATTRIBUTE_TYPES = {
  metadata: YummyGuide::Administrate::Fields::JsonPrettyField,
  item: YummyGuide::Administrate::Fields::VersionItemField.with_options(namespace: :admin),
  whodunnit: YummyGuide::Administrate::Fields::VersionWhodunnitField.with_options(
    namespace: :admin,
    user_class: "User"
  ),
  pictures: YummyGuide::Administrate::Fields::Area::PictureField
}.freeze

各 field の用途

JsonPrettyField

JSON 文字列または JSON 互換オブジェクトを、整形済みの文字列として表示します。

VersionItemField

PaperTrail の itemreify 結果をもとに、対象 resource のラベルと詳細画面への リンクを表示します。namespace:admin が既定です。

VersionWhodunnitField

PaperTrail の whodunnit から操作ユーザーを解決して表示します。

  • 既定では User クラスを参照します
  • user_class で別クラスを指定できます
  • user_label に proc を渡すと表示ラベルを上書きできます

Area::PictureField

画像添付用の field です。Active Storage の添付オブジェクト、または attachments を返すオブジェクトを前提としています。

必要に応じて以下の option を指定できます。

  • max_uploads
  • input_name
  • purge_input_name
  • attachment_url
  • preview_url

参照

リリース手順

このリポジトリは bundler/gem_tasks を利用しているため、標準の gem リリースタスク で公開できます。

  1. lib/yummy_guide/administrate/version.rbVERSION を更新する
  2. 必要な変更を commit 済みの状態にする
  3. 必要に応じて bundle exec rake spec で確認する
  4. bundle exec rake release を実行する

bundle exec rake release を実行すると、現在の VERSION をもとに v<version> の git tag を作成し、gem を build して rubygems.org へ push します。

事前に RubyGems への push 権限があること、ローカル環境で gem push が利用できる ことを確認してください。

注意点

  • asset は precompile 対象に追加されますが、ホストアプリ側での読み込み設定は別途必要です
  • VersionWhodunnitField は対象ユーザー class や表示ラベルをアプリ事情に合わせて調整してください
  • Area::PictureField の URL 解決は、必要なら option で明示的に上書きしてください