diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 63af62a7ca6..ab4f5594876 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -22,7 +22,7 @@ class ApplicationController < ActionController::Base
before_action :session_expiry, :update_activity_time, :unless => proc { |c| c.remote_user_provided? || c.api_request? }
before_action :set_taxonomy, :require_mail, :check_empty_taxonomy
before_action :authorize
- before_action :welcome, :only => :index, :unless => :api_request?
+ before_action :welcome, :find_selected_columns, :only => :index, :unless => :api_request?
prepend_before_action :allow_webpack, if: -> { Rails.configuration.webpack.dev_server.enabled }
around_action :set_timezone
diff --git a/app/controllers/concerns/application_shared.rb b/app/controllers/concerns/application_shared.rb
index 88c8aebc423..e6449bfa662 100644
--- a/app/controllers/concerns/application_shared.rb
+++ b/app/controllers/concerns/application_shared.rb
@@ -140,4 +140,8 @@ def find_session_taxonomy(taxonomy, user)
end
determined_taxonomy
end
+
+ def find_selected_columns
+ @selected_columns = Foreman::SelectableColumns::Storage.selected_by(User.current, controller_name)
+ end
end
diff --git a/app/controllers/hosts_controller.rb b/app/controllers/hosts_controller.rb
index 6e7b4206250..9af3fb72a1d 100644
--- a/app/controllers/hosts_controller.rb
+++ b/app/controllers/hosts_controller.rb
@@ -33,6 +33,8 @@ class HostsController < ApplicationController
before_action :set_host_type, :only => [:update]
before_action :find_multiple, :only => MULTIPLE_ACTIONS
before_action :validate_power_action, :only => :update_multiple_power_state
+ # index action is already included in ApplicationController
+ before_action(:only => SEARCHABLE_ACTIONS.without('index')) { find_selected_columns }
helper :hosts, :reports, :interfaces
diff --git a/app/helpers/selectable_columns_helper.rb b/app/helpers/selectable_columns_helper.rb
new file mode 100644
index 00000000000..9207b02ea0a
--- /dev/null
+++ b/app/helpers/selectable_columns_helper.rb
@@ -0,0 +1,52 @@
+module SelectableColumnsHelper
+ def render_selected_column_ths
+ result = ""
+ @selected_columns.each do |column|
+ result += render(
+ 'common/selectable_column_th',
+ attributes: attributes(column[:th]),
+ th_content: th_content(column),
+ callback: column[:th][:callback]
+ )
+ end
+ result.html_safe
+ end
+
+ def render_selected_column_tds(record)
+ result = ""
+ @selected_columns.each do |column|
+ result += render(
+ 'common/selectable_column_td',
+ attributes: attributes(column[:td]),
+ attr_callbacks: column[:td][:attr_callbacks],
+ subject: record,
+ callback: column[:td][:callback]
+ )
+ end
+ result.html_safe
+ end
+
+ def attr_from_callbacks(callbacks, subject)
+ return unless callbacks
+
+ callbacks.map { |(k, v)| "#{k}=\"#{instance_exec(subject, &v)}\"" }
+ .join(' ')
+ .html_safe
+ end
+
+ private
+
+ def attributes(th_or_td)
+ th_or_td.except(:callback, :sortable, :default_sort, :attr_callbacks, :label)
+ .map { |(k, v)| "#{k}=\"#{v}\"" }
+ .join(' ')
+ .html_safe
+ end
+
+ def th_content(col)
+ return if col[:th][:callback]
+ return col[:th][:label] unless col[:th][:sortable]
+
+ sort col[:key], as: col[:th][:label], default: col[:th][:default_sort] || 'ASC'
+ end
+end
diff --git a/app/registries/foreman/plugin.rb b/app/registries/foreman/plugin.rb
index bb82899959d..44b18ef2078 100644
--- a/app/registries/foreman/plugin.rb
+++ b/app/registries/foreman/plugin.rb
@@ -329,6 +329,10 @@ def settings(&block)
SettingManager.define(id, &block)
end
+ def selectable_columns(table, &block)
+ Foreman::SelectableColumns::Storage.register(table, &block)
+ end
+
def security_block(name, &block)
@security_block = name
instance_eval(&block)
diff --git a/app/registries/foreman/selectable_columns/category.rb b/app/registries/foreman/selectable_columns/category.rb
new file mode 100644
index 00000000000..55d143663fa
--- /dev/null
+++ b/app/registries/foreman/selectable_columns/category.rb
@@ -0,0 +1,63 @@
+module Foreman
+ module SelectableColumns
+ class Category < Array
+ attr_reader :id, :label
+
+ def initialize(id, label, table, default: false)
+ @id = id
+ @label = label
+ @table = table
+ @default = default
+ @columns_to_use = HashWithIndifferentAccess.new
+ super(0)
+ end
+
+ def column(key, th:, td:)
+ self << { key: key.to_s, th: th, td: td }.with_indifferent_access
+ end
+
+ def use_column(key, from:)
+ return if from.to_s == @id
+
+ @columns_to_use[from] ||= []
+ @columns_to_use[from] << key.to_s
+ end
+
+ def common_th_class
+ 'hidden-tablet hidden-xs'
+ end
+
+ def common_td_class
+ common_th_class + ' ellipsis'
+ end
+
+ # This instance can be updated after it was created by DSL, but
+ # only at the initialization time.
+ # This method is only for actual runtime usage,
+ # after the Storage is fully defined/updated.
+ # Thus, saving once computed result.
+ def keys
+ @keys ||= columns.map { |c| c[:key] }.sort
+ end
+
+ def default?
+ @default
+ end
+
+ def columns
+ self + foreign_columns
+ end
+
+ private
+
+ # This is meant for actual runtime usage,
+ # after the categories are fully defined.
+ # Thus, saving once computed result.
+ def foreign_columns
+ @foreign_columns ||= @columns_to_use.keys.map do |category_id|
+ @table.find { |cat| cat.id == category_id }&.select { |col| @columns_to_use[category_id].include?(col[:key]) }
+ end.flatten.compact
+ end
+ end
+ end
+end
diff --git a/app/registries/foreman/selectable_columns/storage.rb b/app/registries/foreman/selectable_columns/storage.rb
new file mode 100644
index 00000000000..6ae9cc14bb0
--- /dev/null
+++ b/app/registries/foreman/selectable_columns/storage.rb
@@ -0,0 +1,57 @@
+module Foreman
+ module SelectableColumns
+ class Storage
+ include Singleton
+
+ class << self
+ def tables
+ @tables ||= ActiveSupport::HashWithIndifferentAccess.new
+ end
+
+ def define(name, &block)
+ return Foreman::Logging.logger('app').warn _('Table %s is already defined, ignoring.') % name if tables[name]
+
+ table = SelectableColumns::Table.new(name)
+ table.instance_eval(&block)
+ tables[name] = table
+ end
+
+ def register(name, &block)
+ return Foreman::Logging.logger('app').warn _('Table %s is not defined, ignoring.') % name unless tables[name]
+
+ tables[name].instance_eval(&block)
+ end
+
+ # This is for UI data mostly
+ def defined_for(table)
+ return Foreman::Logging.logger('app').warn _('Table %s is not defined, ignoring.') % table unless tables[table]
+
+ tables[table].map do |category|
+ {
+ id: category.id,
+ name: category.label,
+ columns: category.columns.map { |c| { id: c[:key], name: c[:th][:label] } },
+ }
+ end
+ end
+
+ def selected_by(user, table)
+ return unless tables[table]
+
+ selected_keys = user.table_preferences.find_by(name: table)&.columns&.sort
+ result = if selected_keys
+ tables[table].select { |category| (category.keys & selected_keys).any? }
+ .map(&:columns)
+ .flatten
+ .select { |col| selected_keys.include?(col[:key]) }
+ else
+ tables[table].select { |category| category.default? }
+ .map(&:columns)
+ .flatten
+ end
+ result.uniq
+ end
+ end
+ end
+ end
+end
diff --git a/app/registries/foreman/selectable_columns/table.rb b/app/registries/foreman/selectable_columns/table.rb
new file mode 100644
index 00000000000..0f0bf4e4f6e
--- /dev/null
+++ b/app/registries/foreman/selectable_columns/table.rb
@@ -0,0 +1,28 @@
+module Foreman
+ module SelectableColumns
+ class Table < Array
+ attr_reader :name
+
+ def initialize(name)
+ @name = name
+ super(0)
+ end
+
+ def category(id, label: _('General'), default: false, &block)
+ category = find_or_create(id.to_s, label, default)
+ category.instance_eval(&block)
+ end
+
+ private
+
+ def find_or_create(id, label, default)
+ category = find { |c| c.id == id }
+ unless category
+ category = Category.new(id, label, self, default: default)
+ self << category
+ end
+ category
+ end
+ end
+ end
+end
diff --git a/app/views/common/_selectable_column_td.html.erb b/app/views/common/_selectable_column_td.html.erb
new file mode 100644
index 00000000000..62795a9e9ac
--- /dev/null
+++ b/app/views/common/_selectable_column_td.html.erb
@@ -0,0 +1 @@
+
<%= attr_from_callbacks(attr_callbacks, subject) %>><%= instance_exec(subject, &callback) %> |
diff --git a/app/views/common/_selectable_column_th.html.erb b/app/views/common/_selectable_column_th.html.erb
new file mode 100644
index 00000000000..ce2beb43a6e
--- /dev/null
+++ b/app/views/common/_selectable_column_th.html.erb
@@ -0,0 +1 @@
+><%= th_content ? th_content : instance_eval(callback) %> |
diff --git a/app/views/hosts/_list.html.erb b/app/views/hosts/_list.html.erb
index 5918e9a0b1f..60c246d94f2 100644
--- a/app/views/hosts/_list.html.erb
+++ b/app/views/hosts/_list.html.erb
@@ -7,14 +7,8 @@
<% if power_status_visible? %>
<%= _('Power') %> |
<% end %>
- <%= sort :name, :as => _('Name') %> |
- <%= sort :os_title, :as => _("Operating system") %> |
- <%= sort :model, :as => _('Model') %> |
- <%= sort :owner, :as => _('Owner') %> |
- <%= sort :hostgroup, :as => _("Host group") %> |
- <%= sort :last_report, :as => _('Last report'), :default => 'DESC' %> |
+ <%= render_selected_column_ths %>
<%= render_pagelets_for(:hosts_table_column_header) %>
- <%= sort :comment, :as => _('Comment') %> |
<%= _('Actions') %> |
@@ -29,15 +23,8 @@
<%= react_component('PowerStatus', id: host.id, url: power_api_host_path(host)) %>
<% end %>
- <%= name_column(host) %>
- |
- <%= (icon(host.operatingsystem, :size => "16x16") + " #{host.operatingsystem.to_label}").html_safe if host.operatingsystem %> |
- <%= host.compute_resource_or_model %> |
- <%= host_owner_column(host) %> |
- <%= label_with_link host.hostgroup, 23, @hostgroup_authorizer %> |
- <%= last_report_column(host) %> |
+ <%= render_selected_column_tds(host) %>
<%= render_pagelets_for(:hosts_table_column_content, :subject => host) %>
- <%= icon_text('comment', '') unless host.comment.empty? %> |
<%= action_buttons(
display_link_if_authorized(_("Edit"), hash_for_edit_host_path(:id => host).merge(:auth_object => host, :authorizer => authorizer)),
diff --git a/config/initializers/foreman_register.rb b/config/initializers/foreman_register.rb
index 8e8fa9e956e..bba4c30177b 100644
--- a/config/initializers/foreman_register.rb
+++ b/config/initializers/foreman_register.rb
@@ -4,3 +4,24 @@
partial: 'hosts/init_config_tab',
priority: 100
end
+
+Foreman::SelectableColumns::Storage.define(:hosts) do
+ category :general, default: true do
+ column :name, th: { label: _('Name'), sortable: true, width: '25%' },
+ td: { class: 'ellipsis', callback: ->(host) { name_column(host) } }
+ column :os_title, th: { label: _('Operating system'), sortable: true, width: '17%', class: 'hidden-xs' },
+ td: { class: 'hidden-xs ellipsis', callback: ->(host) { (icon(host.operatingsystem, size: "16x16") + " #{host.operatingsystem.to_label}").html_safe if host.operatingsystem } }
+ column :model, th: { label: _('Model'), sortable: true, width: '10%', class: common_th_class },
+ td: { class: common_td_class, callback: ->(host) { host.compute_resource_or_model } }
+ column :owner, th: { label: _('Owner'), sortable: true, width: '8%', class: common_th_class },
+ td: { class: common_td_class, callback: ->(host) { host_owner_column(host) } }
+ column :hostgroup, th: { label: _('Host group'), sortable: true, width: '15%', class: common_th_class },
+ td: { class: common_th_class, callback: ->(host) { label_with_link host.hostgroup, 23, @hostgroup_authorizer } }
+ column :last_report, th: { label: _('Last report'), sortable: true, default_sort: 'DESC', width: '10%', class: common_th_class },
+ td: { class: common_td_class, callback: ->(host) { last_report_column(host) } }
+ column :comment, th: { label: _('Comment'), sortable: true, width: '7%', class: common_th_class },
+ td: { class: common_th_class + ' ca',
+ attr_callbacks: { title: ->(host) { host.comment&.truncate(255) } },
+ callback: ->(host) { icon_text('comment', '') unless host.comment.empty? } }
+ end
+end
diff --git a/developer_docs/how_to_create_a_plugin.asciidoc b/developer_docs/how_to_create_a_plugin.asciidoc
index cae3bc6edf5..bbcffd17f42 100644
--- a/developer_docs/how_to_create_a_plugin.asciidoc
+++ b/developer_docs/how_to_create_a_plugin.asciidoc
@@ -429,6 +429,108 @@ widget_name". The content of the widget should be in the path:
app/views/dashboard/_.html.erb
....
+[[selectable-columns]]
+=== Selectable columns
+
+_Requires Foreman 3.4 or higher, set `requires_foreman '>= 3.4'` in engine.rb or
+register.rb._
+
+[[adding-selectable-columns]]
+===== Adding selectable columns to index pages
+
+Index pages always contain a table with a certain set of columns.
+Since Foreman 3.4 some pages have this set extendable, e.g. hosts index page,
+and provide users a way to select which columns are meant to be displayed. For
+the list of available tables with selectable columns, please refer to
+config/initializers/foreman_register.rb file.
+
+
+If you want to add more selectable columns to an existing page in Foreman,
+please the following syntax in
+`Foreman::Plugin.register :foreman_plugin do ... end` block:
+
+[source, ruby]
+----
+selectable_columns :table do
+ category :id, label: N_('Category name'), default: true do
+ column :key,
+ th: {
+ label: N_('Column name'),
+ sortable: true,
+ default_sort: 'DESC',
+ width: '10%',
+ class: 'hidden ca'
+ },
+ td: {
+ class: 'hidden ca',
+ attr_callbacks: {
+ title: ->(record) { record.value }
+ },
+ callback: ->(record) { record.value }
+ }
+ use_column :key, from :category_id
+ end
+end
+----
+
+Where for `category`:
+
+* `id`: [Symbol] (Required) Internal id of the category. Used to find an existing or define a new one.
+* `label`: [String] (Optional) Translated string with category name. Required only for new categories.
+* `default`: [Boolean] (Optional) Flag whether this category should be visible to users by default. Default: `false`.
+
+Where for `column`:
+
+* `key`: [Symbol] (Required) Record attribute's name. Used to make column sortable and selectable.
+* `th`: [Hash] (Required) Column header definition. Please, see below for further information.
+* `td`: [Hash] (Required) Column data definition. Please, see below for further information.
+
+Where for `th`:
+
+* `label`: [String] (Required) Translated string with column name to be shown.
+* `sortable`: [Boolean] (Optional) Makes column sortable. Default: `false`.
+* `default_sort`: [String] (Optional) Defines default sort order. Default: `'ASC'`
+
+Where for `td`:
+
+* `callback`: [Lambda] (Required) A callback that will be used to process data to be shown.
+* `attr_callbacks`: [Hash] (Optional) Only use if you need to process values for HTML attributes dynamically.
+
+Where for `use_column`:
+
+* `key`: [Symbol] (Required) Key of the defined column to include in the current category.
+* `from`: [Symbol] (Required) ID of the category defining the column to use. Category must be in the same table.
+
+You can also define HTML attributes for `` and ` | ` tags. Some common are
+listed below, but you can use your own.
+_Note: all except `:callback`, `:sortable`, `:default_sort`, `:attr_callbacks`
+are considered as HTML tag attributes.
+
+* `width`: [String] (Optional) Width of the column.
+* `class`: [String] (Optional) Space-separated list of CSS classes to use for the column.
+
+[[creating-selectable-columns]]
+===== Creating selectable columns on index pages
+
+If you want to not only extend existing pages, but to also have pages with
+selectable columns in the plugin itself, you can use
+`Foreman::SelectableColumns::Storage` directly. In this case everything the same
+except you no longer need to use plugin's DSL, but the storage itself to define
+your tables:
+
+[source, ruby]
+----
+# in your initializer
+Foreman::SelectableColumns::Storage.define(:table) do
+ category ...
+end
+----
+
+To use your columns in views, please use
+
+* `render_selected_column_ths`: Renders all selectable column | tags for table.
+* `render_selected_column_tds(record)`: Renders all selectable column | tags for table.
+
[[adding-a-pagelet]]
=== Adding a Pagelet
diff --git a/test/helpers/selectable_columns_helper_test.rb b/test/helpers/selectable_columns_helper_test.rb
new file mode 100644
index 00000000000..a1a30c2e45e
--- /dev/null
+++ b/test/helpers/selectable_columns_helper_test.rb
@@ -0,0 +1,39 @@
+require 'test_helper'
+
+class SelectableColumnsHelperTest < ActionView::TestCase
+ include SelectableColumnsHelper
+
+ let(:selected_columns) do
+ [
+ {
+ key: 'key1', th: { label: 'Key1', width: '5%' },
+ td: { class: 'elipsis', callback: ->(o) { o.even? } }
+ }.with_indifferent_access,
+ { key: 'key2', th: { label: 'Key2', class: 'elipsis', width: '10%' },
+ td: { class: 'hidden', attr_callbacks: { title: ->(o) { o.to_s } }, callback: ->(o) { o.odd? } }
+ }.with_indifferent_access,
+ ]
+ end
+
+ setup do
+ @selected_columns = selected_columns
+ end
+
+ test 'should render ths' do
+ expected = [
+ ' | Key1 | ',
+ 'Key2 | ',
+ '',
+ ].join("\n").html_safe
+ assert_equal expected, render_selected_column_ths
+ end
+
+ test 'should render tds' do
+ expected = [
+ 'false | ',
+ 'true | ',
+ '',
+ ].join("\n").html_safe
+ assert_equal expected, render_selected_column_tds(123)
+ end
+end
diff --git a/test/unit/selectable_columns/category_test.rb b/test/unit/selectable_columns/category_test.rb
new file mode 100644
index 00000000000..a3abf7004e7
--- /dev/null
+++ b/test/unit/selectable_columns/category_test.rb
@@ -0,0 +1,53 @@
+require 'test_helper'
+
+class CategoryTest < ActiveSupport::TestCase
+ let(:table) { Foreman::SelectableColumns::Table.new(name: 'foo') }
+
+ test 'should store column definitions' do
+ category = Foreman::SelectableColumns::Category.new(:test, 'Test', table)
+ category.column :key, th: {}, td: {}
+ category.column :key2, th: {}, td: {}
+
+ assert_not_empty category.columns
+ end
+
+ test 'should re-use column definitions' do
+ shared_table = table
+ category1 = Foreman::SelectableColumns::Category.new('test', 'Test', shared_table)
+ category1.column :key, th: {}, td: {}
+ category2 = Foreman::SelectableColumns::Category.new('test2', 'Test2', shared_table)
+ category2.use_column :key, from: :test
+ shared_table.concat([category1, category2])
+
+ assert_not_empty category2.columns
+ end
+
+ test 'should ignore non-existing category for re-usage' do
+ shared_table = table
+ category2 = Foreman::SelectableColumns::Category.new('test2', 'Test2', shared_table)
+ category2.use_column :key, from: :test3
+ shared_table.concat([category2])
+
+ assert_empty category2.columns
+ end
+
+ test 'should ignore non-existing column for re-usage' do
+ shared_table = table
+ category1 = Foreman::SelectableColumns::Category.new('test', 'Test', shared_table)
+ category1.column :key, th: {}, td: {}
+ category2 = Foreman::SelectableColumns::Category.new('test2', 'Test2', shared_table)
+ category2.use_column :key2, from: :test
+ shared_table.concat([category1, category2])
+
+ assert_empty category2.columns
+ end
+
+ test 'should ignore itself for re-usage' do
+ shared_table = table
+ category2 = Foreman::SelectableColumns::Category.new('test2', 'Test2', shared_table)
+ category2.use_column :key, from: :test2
+ shared_table.concat([category2])
+
+ assert_empty category2.columns
+ end
+end
diff --git a/test/unit/selectable_columns/storage_test.rb b/test/unit/selectable_columns/storage_test.rb
new file mode 100644
index 00000000000..8c682f78f4a
--- /dev/null
+++ b/test/unit/selectable_columns/storage_test.rb
@@ -0,0 +1,136 @@
+require 'test_helper'
+
+class StorageTest < ActiveSupport::TestCase
+ setup do
+ Foreman::SelectableColumns::Storage.stubs(tables: HashWithIndifferentAccess.new)
+ end
+
+ test 'should store table definitions' do
+ Foreman::SelectableColumns::Storage.define(:default) do
+ category(:general) {}
+ end
+ Foreman::SelectableColumns::Storage.define(:nondefault) do
+ category(:nongeneral) {}
+ end
+
+ assert_not_empty Foreman::SelectableColumns::Storage.tables
+ end
+
+ test 'should not re-define a table' do
+ only_name = 'general'
+ Foreman::SelectableColumns::Storage.define(:default) do
+ category(only_name) {}
+ end
+ Foreman::SelectableColumns::Storage.define(:default) do
+ category(:general2) {}
+ end
+ table = Foreman::SelectableColumns::Storage.tables[:default]
+
+ assert_equal 1, table.size
+ assert_equal only_name, table.first.id
+ end
+
+ test 'should re-use defined table' do
+ Foreman::SelectableColumns::Storage.define(:default) do
+ category(:general) {}
+ end
+ Foreman::SelectableColumns::Storage.register(:default) do
+ category(:nongeneral) {}
+ end
+ table = Foreman::SelectableColumns::Storage.tables[:default]
+
+ assert_equal 2, table.size
+ end
+
+ test 'should not define a table while registering categories' do
+ Foreman::SelectableColumns::Storage.register(:default) do
+ category(:nongeneral) {}
+ end
+
+ assert_empty Foreman::SelectableColumns::Storage.tables
+ end
+
+ test 'should return defined columns on a table in simplified form' do
+ expected = [
+ {
+ id: 'general',
+ name: 'General',
+ columns: [
+ { id: 'key1', name: 'Key1' },
+ { id: 'key2', name: 'Key2' },
+ ],
+ },
+ {
+ id: 'nongeneral',
+ name: 'Nongeneral',
+ columns: [
+ { id: 'key3', name: 'Key3' },
+ { id: 'key1', name: 'Key1' },
+ ],
+ },
+ ]
+
+ Foreman::SelectableColumns::Storage.define(:default) do
+ category :general do
+ column :key1, th: { label: 'Key1' }, td: {}
+ column :key2, th: { label: 'Key2' }, td: {}
+ end
+ category :nongeneral, label: 'Nongeneral' do
+ column :key3, th: { label: 'Key3' }, td: {}
+ use_column :key1, from: :general
+ end
+ end
+
+ result = Foreman::SelectableColumns::Storage.defined_for(:default)
+ assert_equal expected, result
+ end
+
+ test 'should return filtered by preferences full definitions of columns' do
+ Foreman::SelectableColumns::Storage.define(:default) do
+ category :general do
+ column :key1, th: { label: 'Key1' }, td: {}
+ column :key2, th: { label: 'Key2' }, td: {}
+ end
+ category :nongeneral, label: 'Nongeneral' do
+ column :key3, th: { label: 'Key3' }, td: {}
+ use_column :key1, from: :general
+ end
+ end
+
+ expected = [
+ { key: 'key1', th: { label: 'Key1' }, td: {} }.with_indifferent_access,
+ { key: 'key3', th: { label: 'Key3' }, td: {} }.with_indifferent_access,
+ ]
+
+ user = FactoryBot.create(:user)
+ user.table_preferences.create(name: 'default', columns: ['key1', 'key3'])
+
+ result = Foreman::SelectableColumns::Storage.selected_by(user, :default)
+
+ assert_equal expected, result
+ end
+
+ test 'should return default category if user does not have preferences' do
+ Foreman::SelectableColumns::Storage.define(:default) do
+ category :general, default: true do
+ column :key1, th: { label: 'Key1' }, td: {}
+ column :key2, th: { label: 'Key2' }, td: {}
+ end
+ category :nongeneral, label: 'Nongeneral' do
+ column :key3, th: { label: 'Key3' }, td: {}
+ use_column :key1, from: :general
+ end
+ end
+
+ expected = [
+ { key: 'key1', th: { label: 'Key1' }, td: {} }.with_indifferent_access,
+ { key: 'key2', th: { label: 'Key2' }, td: {} }.with_indifferent_access,
+ ]
+
+ user = FactoryBot.create(:user)
+
+ result = Foreman::SelectableColumns::Storage.selected_by(user, :default)
+
+ assert_equal expected, result
+ end
+end
diff --git a/test/unit/selectable_columns/table_test.rb b/test/unit/selectable_columns/table_test.rb
new file mode 100644
index 00000000000..005b6336b18
--- /dev/null
+++ b/test/unit/selectable_columns/table_test.rb
@@ -0,0 +1,19 @@
+require 'test_helper'
+
+class TableTest < ActiveSupport::TestCase
+ test 'should store category definitions' do
+ table = Foreman::SelectableColumns::Table.new(:test)
+ table.category(:test) {}
+ table.category(:test2) {}
+
+ assert_not_empty table
+ end
+
+ test 'should re-use category definitions' do
+ table = Foreman::SelectableColumns::Table.new(:test)
+ table.category(:test) {}
+ table.category(:test) {}
+
+ assert_equal 1, table.size
+ end
+end
|