app/helpers/table_builder_helper.rb
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