af83/chouette-core

View on GitHub
app/helpers/table_builder_helper.rb

Summary

Maintainability
F
4 days
Test Coverage
require 'table_builder_helper/column'
require 'table_builder_helper/url'

# table_builder_2
# A Rails helper that constructs an HTML table from a collection of objects. It
# receives the collection and an array of columns that get transformed into
# `<td>`s. A column of checkboxes can be added to the left side of the table
# for multiple selection. Columns are sortable by default, but sorting can be
# disabled either at the table level or at the column level. An optional
# `links` argument takes a set of symbols corresponding to controller actions
# that should be inserted in a gear menu next to each row in the table. That
# menu will also be populated with links defined in `collection#action_links`,
# a list of `Link` objects defined in a decorator for the given object.
#
# Depends on `params` and `current_referential`.
#
# Example:
#   table_builder_2(
#     @companies,
#     [
#       TableBuilderHelper::Column.new(
#         name: 'ID Codif',
#         attribute: Proc.new { |n| n.try(:objectid).try(:local_id) },
#         sortable: false
#       ),
#       TableBuilderHelper::Column.new(
#         key: :name,
#         attribute: 'name',
#         link_to: lambda do |company|
#           referential_company_path(@referential, company)
#         end
#       ),
#       TableBuilderHelper::Column.new(
#         key: :phone,
#         attribute: 'phone'
#       ),
#       TableBuilderHelper::Column.new(
#         key: :email,
#         attribute: 'email'
#       ),
#       TableBuilderHelper::Column.new(
#         key: :url,
#         attribute: 'url'
#       ),
#     ],
#     links: [:show, :edit],
#     cls: 'table has-search',
#     overhead: [
#       {
#         title: 'one',
#         width: 1,
#         cls: 'toto'
#       },
#       {
#         title: 'two <span class="test">Info</span>',
#         width: 2,
#         cls: 'default'
#       }
#     ]
#   )
module TableBuilderHelper
  # TODO: rename this after migration from `table_builder`
  def table_builder_2(
    # An `ActiveRecord::Relation`, wrapped in a decorator to provide a list of
    # `Link` objects via an `#action_links` method
    collection,

    # An array of `TableBuilderHelper::Column`s
    columns,

    # When false, no columns will be sortable
    sortable: true,

    # When true, adds a column of checkboxes to the left side of the table
    selectable: false,

    # A set of controller actions that will be added as links to the top of the
    # gear menu
    links: [],

    # A CSS class to apply to the <table>
    cls: '',

    # A set of content, over the th line...
    overhead: [],

    # Possibility to override the result of collection.model
    model: nil,

    #overrides the params[:action] value
    action: nil

  )
    content_tag :table,
      thead(collection, columns, sortable, selectable, links.any?, overhead, model || collection.model, action || params[:action]) +
        tbody(collection, columns, selectable, links, overhead, model, action || params[:action]),
      class: cls
  end

  def self.item_row_class_name collection, model=nil
    model_name = model&.name

    model_name ||=
      if collection.respond_to?(:model)
        collection.model.name
      elsif collection.respond_to?(:first)
        collection.first.class.name
      else
        "item"
      end

    model_name.split("::").last.parameterize
  end

  private

  def thead(collection, columns, sortable, selectable, has_links, overhead, model, action)
    content_tag :thead do
      # Inserts overhead content if any specified
      over_head = ''

      unless overhead.empty?
        over_head = content_tag :tr, class: 'overhead' do
          oh_cont = []

          overhead.each do |h|
            oh_cont << content_tag(:th, raw(h[:title]), colspan: h[:width], class: h[:cls])
          end
          oh_cont.join.html_safe
        end
      end

      main_head = content_tag :tr do
        hcont = []

        if selectable
          hcont << content_tag(:th, checkbox(id_name: '0', value: 'all'))
        end

        columns.each do |column|
          if overhead.empty?
            hcont << content_tag(:th, build_column_header(
              column,
              sortable,
              model,
              params
            ))

          else
            i = columns.index(column)

            if overhead[i].blank?
              prev = nil
              if i > 0
                (i-1..0).each do |j|
                  o = overhead[j]
                  if (j + o[:width].to_i) >= i
                    prev = o
                    break
                  end
                end
              end
              if prev
                clsArrayH = overhead[i - 1][:cls].split

                hcont << content_tag(:th, build_column_header(
                  column,
                  sortable,
                  model,
                  params
                ), class: td_cls(clsArrayH))

              else
                hcont << content_tag(:th, build_column_header(
                  column,
                  sortable,
                  model,
                  params
                ))
              end

            else
              clsArrayH = overhead[i][:cls].split

              hcont << content_tag(:th, build_column_header(
                column,
                sortable,
                model,
                params
              ), class: td_cls(clsArrayH))

            end

          end
        end

        # Inserts a blank column for the gear menu
        last_item = collection.first
        action_links = last_item && last_item.respond_to?(:action_links) && (last_item&.action_links&.is_a?(AF83::Decorator::ActionLinks) ? last_item.action_links(action) : last_item.action_links)
        if has_links || action_links.try(:any?)
          hcont << content_tag(:th, '')
        end

        hcont.join.html_safe
      end

      (over_head + main_head).html_safe
    end
  end

  def tr item, columns, selectable, links, overhead, model_name, action
    klass = "#{model_name} #{model_name}-#{item.id}"
    content_tag :tr, class: klass do
      bcont = []
      if selectable
        disabled = selectable.respond_to?(:call) && !selectable.call(item)
        bcont << content_tag(
          :td,
          checkbox(id_name: item.try(:id), value: item.try(:id), disabled: disabled)
        )
      end

      columns.each do |column|
        value = column.value(item)
        extra_class = column.td_class(item)

        if column.linkable?
          path = column.link_to(item)
          link = value.present? && path.present? ? link_to(value, path) : value

          if overhead.empty?
            bcont << content_tag(:td, link, title: 'Voir', class: extra_class)

          else
            i = columns.index(column)

            if overhead[i].blank?
              prev = nil
              if i > 0
                (i-1..0).each do |j|
                  o = overhead[j]
                  if (j + o[:width].to_i) >= i
                    prev = o
                    break
                  end
                end
              end
              if prev
                clsArrayAlt = overhead[i - 1][:cls].split

                bcont << content_tag(:td, link, title: 'Voir', class: td_cls(clsArrayAlt, extra_class))

              else
                bcont << content_tag(:td, link, title: 'Voir', class: extra_class)
              end

            else
              clsArray = overhead[columns.index(column)][:cls].split

              bcont << content_tag(:td, link, title: 'Voir', class: td_cls(clsArray, extra_class))
            end
          end

        else
          if overhead.empty?
            bcont << content_tag(:td, value, class: extra_class)

          else
            i = columns.index(column)

            if overhead[i].blank?
              if (i > 0) && (overhead[i - 1][:width].to_i > 1)
                clsArrayAlt = overhead[i - 1][:cls].split

                bcont << content_tag(:td, value, class: td_cls(clsArrayAlt, extra_class))

              else
                bcont << content_tag(:td, value, class: extra_class)
              end

            else
              clsArray = overhead[i][:cls].split

              bcont << content_tag(:td, value, class: td_cls(clsArray))
            end
          end
        end
      end

      action_links = item && item.respond_to?(:action_links) && (item.action_links.is_a?(AF83::Decorator::ActionLinks) ? item.action_links(action) : item.action_links)

      if links.any? || action_links.try(:any?)
        bcont << content_tag(
          :td,
          build_links(item, links, action),
          class: 'actions'
        )
      end

      bcont.join.html_safe
    end
  end

  def tbody(collection, columns, selectable, links, overhead, model = nil, action)
    model_name = TableBuilderHelper.item_row_class_name collection, model

    content_tag :tbody do
      collection.map do |item|
        tr item, columns, selectable, links, overhead, model_name, action
      end.join.html_safe
    end
  end

  def td_cls(a, extra_class="")
    out = [extra_class]
    if a.include? 'full-border'
      a.slice!(a.index('full-border'))

      out += a
    end
    out = out.select(&:present?).join(' ')
    out.present? ? out : nil
  end

  def build_links(item, links, action)
    trigger = content_tag(
      :div,
      class: 'btn dropdown-toggle',
      data: { toggle: 'dropdown' }
    ) do
      content_tag :span, '', class: 'fa fa-cog'
    end

    action_links = item.action_links
    if action_links.is_a?(AF83::Decorator::ActionLinks)
      menu = content_tag :div, class: 'dropdown-menu' do
        item.action_links(action).grouped_by(:primary, :secondary, :footer).map do |group, _links|
          if _links.any?
            content_tag :ul, class: group do
              _links.map{|link| gear_menu_link(link)}.join.html_safe
            end
          end
        end.join.html_safe
      end
    end

    content_tag :div, trigger + menu, class: 'btn-group'
  end

  def build_column_header(
        column,
        table_is_sortable,
        model,
        params
      )

    sort_on = sort_direction = nil

    # Try to detect a current Search order
    if (order = @search.try(:order))
      sort_on, sort_direction = order.attributes.first
    end

    # Legacy sort parameters
    sort_on ||= params[:sort]
    sort_direction ||= params[:direction]

    # The following code only supports string values
    sort_on = sort_on.to_s if sort_on
    sort_direction = sort_direction.to_s if sort_direction

    if !table_is_sortable || !column.sortable
      return column.header_label(model)
    end

    direction =  if sort_direction == 'desc'
        'asc'
      else
        'desc'
      end

    active = column.key.to_s == sort_on

    link_to(params.permit!.merge({direction: direction, sort: column.key})) do
      arrow_up = content_tag(
        :span,
        '',
        class: "fa fa-sort-up #{active && direction == 'desc' ? 'active' : ''}"
      )
      arrow_down = content_tag(
        :span,
        '',
        class: "fa fa-sort-down #{active && direction == 'asc' ? 'active' : ''}"
      )

      arrow_icons = content_tag :span, arrow_up + arrow_down, class: 'orderers'

      (
        column.header_label(model) +
        arrow_icons
      ).html_safe
    end
  end

  def checkbox(id_name:, value:, disabled: false)
    content_tag :div, '', class: 'checkbox' do
      check_box_tag(id_name, value, nil, disabled: disabled).concat(
        content_tag(:label, '', for: id_name)
      )
    end
  end

  def gear_menu_link(link)
    klass = []
    klass << link.extra_class if link.extra_class
    klass << 'delete-action' if link.method == :delete
    klass << 'disabled' if link.disabled
    content_tag(
      :li,
      link_to(
        link.disabled ? '#' : link.href,
        method: link.disabled ? nil : link.method,
        data: link.data,
        disabled: link.disabled,
        download: link.html_options[:download]
      ) do
        link.content
      end,
      class: (klass.join(' ') if klass.present?)
    )
  end

  def referential
    # Certain controllers don't define a `#current_referential`. In these
    # cases, avoid a `NoMethodError`.
    @__referential__ ||= try(:current_referential)
  end

  def workgroup
    # Certain controllers don't define a `#current_referential`. In these
    # cases, avoid a `NoMethodError`.
    @__workgroup__ ||= try(:current_workgroup)
  end
end