Патчим ActiveAdmin

Есть определённый класс задач которые нельзя решить простым изменением настроек, или созданием дополнительного компонента, иногда приходится внедриться в сам код ActiveAdmin. Есть несколько вариантов, можно форкать проект и модифицировать код под себя и если изменение несёт ценность остальным его можно предложить как пулл-реквест, но бывают необходимы совершенно кастомные вещи, здесь приходится прибегать к monkey patching.

Column margin

Есть такой компонент колонки он позволяет развивать страницу таким образом:

columns do
  column span: 2 do
  end
  
  column span: 1 do
  end
end

Это генерирует вот такой HTML

<div class="columns">
  <div class="column" style="width: 66.0%; margin-right: 2%;"></div>
  <div class="column" style="width: 32.0%;"></div>
</div>

Что если я хочу уменьшить margin, 2% съедает очень много места. Большинство подобных проблем решается с помощью стилей, но в данном случае размер захардкожен в методе ActiveAdmin::Views::Columns#margin_size. Здесь нам поможет monkey pathing.

# config/initializers/active_admin.rb
ActiveAdmin::Views::Columns.prepend(ActiveAdmin::Views::CustomColumns)

# lib/active_admin/views/custom_columns.rb
module ActiveAdmin
  module Views
    module CustomColumns
      def margin_size
        0.5
      end
    end
  end
end

Есть эпичный ответ про money pathing на stackoverflow. prepend подмешивает наш модуль к классу так что что при поиске метода сначала он будет найден в нашем модуле, далее используя super можно вызвать оригинальную реализацию если она нам нужна. Используя эту технику покажу ещё пару примеров

Вставка <script> тега на страницу

Бывает что нужно использовать какие-то внешние скрипты, их можно вставить в хеадер или футер.

# config/initializers/active_admin.rb
ActiveAdmin::Views::Footer.prepend(ActiveAdmin::Views::WithYandexMaps)

# lib/active_admin/views/with_yandex_maps.rb
module ActiveAdmin
  module Views
    module WithYandexMaps
      def build(*args)
        super

        text_node javascript_include_tag(map_js_link, async: true)
      end
    end    
  end
end

Здесь мы используем базовую реализацию и добавляем ноду с тегом script, в результате получаем:

<div class="footer" id="footer">
  <p>
    Powered by 
    <a href="http://www.activeadmin.info">Active Admin</a> 
    1.4.3
  </p>
  <script src="http://api-maps.yandex.ru/2.1/?lang=ru_RU" async="async">    
  </script>
</div>

Используем атрибут async для того чтобы браузер не ждал загрузки скрипта и обрабатывал страницу дальше.

Кастомные элементы в header

А теперь вообще безумие. Что если в вашем приложении есть некие домены или tenants, например регионы, и что если админы одного региона не должны работать в другом и даже знать про него, но есть некие супер-админы которые должны знать обо всём и видеть всё с удобным способ переключения. Самое логичное это добавить в заголовку selectbox с возможностью выбора региона.

# config/initializers/active_admin.rb
ActiveAdmin::Views::Header.prepend(ActiveAdmin::Views::CustomHeader)

# lib/active_admin/views/custom_header.rb
module ActiveAdmin
  module Views
    module CustomHeader
      def utility_navigation(*args)
        insert_tag Arbre::HTML::Ul, class: 'utility-navigation-table' do
          insert_tag(
            Arbre::HTML::Form, 
            class: 'current-region-form', 
            action: '/admin/regions/select', 
            method: :patch
          ) do
            insert_tag Arbre::HTML::Select, id: 'current-region' do
              # Регионы доступные текущему админу
              current_active_admin_user.regions.each do |region|
                selected = 
                  if current_active_admin_user.current_region == region
                    'selected'
                  end

                insert_tag(
                  Arbre::HTML::Option, 
                  region.name, 
                  value: region.id, 
                  selected: selected # Помечаем текущий регион
                )
              end
            end
          end
        end

        super
      end
    end
  end
end

Добавляем форму с выпадающим списком в utility_navigation это блок внутри header. И обрабатываем HTTP запрос при переключении:

# app/admin/regions.rb
ActiveAdmin.register Region do
  # ...

  collection_action :current_region, method: :patch do
    region = Region.find(params[:region_id])

    if admin_panel_user.region_ids.include?(region.id)
      admin_panel_user.update!(current_region: region)
      render json: {}, status: 200
    else
      render json: {}, status: 403
    end
  end
end

На этот раз пришлось немного повозится со стилями чтобы это нормально выглядело, но результат того стоил:

В прошлую неделю эпизодически работал над адаптацией ActiveAdmin под мобильные устройства, надеюсь в ближайшее время закончить и поделиться рецептом.