katalyst-tables 2.6.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -1
  3. data/README.md +57 -213
  4. data/app/assets/builds/katalyst/tables.esm.js +17 -47
  5. data/app/assets/builds/katalyst/tables.js +17 -47
  6. data/app/assets/builds/katalyst/tables.min.js +1 -1
  7. data/app/assets/builds/katalyst/tables.min.js.map +1 -1
  8. data/app/assets/stylesheets/katalyst/tables/_index.scss +1 -0
  9. data/app/assets/stylesheets/katalyst/tables/_ordinal.scss +38 -0
  10. data/app/assets/stylesheets/katalyst/tables/_table.scss +123 -0
  11. data/app/assets/stylesheets/katalyst/tables/typed-columns/_boolean.scss +4 -0
  12. data/app/assets/stylesheets/katalyst/tables/typed-columns/_currency.scss +5 -0
  13. data/app/assets/stylesheets/katalyst/tables/typed-columns/_date.scss +4 -0
  14. data/app/assets/stylesheets/katalyst/tables/typed-columns/_datetime.scss +4 -0
  15. data/app/assets/stylesheets/katalyst/tables/typed-columns/_index.scss +5 -0
  16. data/app/assets/stylesheets/katalyst/tables/typed-columns/_number.scss +5 -0
  17. data/app/components/concerns/katalyst/tables/has_table_content.rb +17 -8
  18. data/app/components/concerns/katalyst/tables/identifiable.rb +51 -0
  19. data/app/components/concerns/katalyst/tables/orderable.rb +35 -105
  20. data/app/components/concerns/katalyst/tables/selectable.rb +18 -74
  21. data/app/components/concerns/katalyst/tables/sortable.rb +51 -17
  22. data/app/components/katalyst/table_component.html.erb +4 -4
  23. data/app/components/katalyst/table_component.rb +277 -47
  24. data/app/components/katalyst/tables/body_row_component.html.erb +5 -0
  25. data/app/components/katalyst/tables/body_row_component.rb +4 -24
  26. data/app/components/katalyst/tables/cell_component.rb +85 -0
  27. data/app/components/katalyst/tables/cells/boolean_component.rb +20 -0
  28. data/app/components/katalyst/tables/cells/currency_component.rb +29 -0
  29. data/app/components/katalyst/tables/cells/date_component.rb +67 -0
  30. data/app/components/katalyst/tables/cells/date_time_component.rb +60 -0
  31. data/app/components/katalyst/tables/cells/number_component.rb +22 -0
  32. data/app/components/katalyst/tables/cells/ordinal_component.rb +44 -0
  33. data/app/components/katalyst/tables/cells/rich_text_component.rb +21 -0
  34. data/app/components/katalyst/tables/cells/select_component.rb +39 -0
  35. data/app/components/katalyst/tables/data.rb +30 -0
  36. data/app/components/katalyst/tables/empty_caption_component.rb +1 -1
  37. data/app/components/katalyst/tables/header_row_component.html.erb +5 -0
  38. data/app/components/katalyst/tables/header_row_component.rb +4 -24
  39. data/app/components/katalyst/tables/label.rb +37 -0
  40. data/app/components/katalyst/tables/orderable/form_component.rb +38 -0
  41. data/app/components/katalyst/tables/selectable/form_component.html.erb +6 -4
  42. data/app/components/katalyst/tables/selectable/form_component.rb +8 -11
  43. data/app/controllers/concerns/katalyst/tables/backend.rb +2 -28
  44. data/app/helpers/katalyst/tables/frontend.rb +48 -2
  45. data/app/javascript/tables/application.js +0 -5
  46. data/app/javascript/tables/orderable/form_controller.js +8 -6
  47. data/app/javascript/tables/orderable/item_controller.js +9 -0
  48. data/app/models/concerns/katalyst/tables/collection/core.rb +6 -1
  49. data/app/models/concerns/katalyst/tables/collection/pagination.rb +8 -1
  50. data/app/models/concerns/katalyst/tables/collection/sorting.rb +85 -17
  51. data/app/models/katalyst/tables/collection/array.rb +38 -0
  52. data/app/models/katalyst/tables/collection/base.rb +4 -0
  53. data/lib/katalyst/tables/config.rb +23 -0
  54. data/lib/katalyst/tables.rb +9 -0
  55. metadata +32 -15
  56. data/app/components/concerns/katalyst/tables/configurable_component.rb +0 -52
  57. data/app/components/concerns/katalyst/tables/turbo_replaceable.rb +0 -79
  58. data/app/components/katalyst/tables/body_cell_component.rb +0 -47
  59. data/app/components/katalyst/tables/header_cell_component.rb +0 -65
  60. data/app/components/katalyst/turbo/pagy_nav_component.rb +0 -23
  61. data/app/components/katalyst/turbo/table_component.rb +0 -45
  62. data/app/helpers/katalyst/tables/frontend/helper.rb +0 -31
  63. data/app/javascript/tables/turbo/collection_controller.js +0 -38
  64. data/app/models/katalyst/tables/collection/sort_form.rb +0 -102
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ class CellComponent < ViewComponent::Base # :nodoc:
6
+ include Katalyst::HtmlAttributes
7
+
8
+ attr_reader :collection, :row, :column, :record
9
+
10
+ def initialize(collection:, row:, column:, record:, label:, heading:, **)
11
+ super(**)
12
+
13
+ @collection = collection
14
+ @row = row
15
+ @column = column
16
+ @record = record
17
+ @heading = heading
18
+
19
+ if @row.header?
20
+ @label = Label.new(collection:, column:, label:)
21
+ else
22
+ @data = Data.new(record:, column:)
23
+ end
24
+ end
25
+
26
+ # @return true if the cell is a heading cell (th).
27
+ def heading?
28
+ @row.header? || @heading
29
+ end
30
+
31
+ # Adds a component to wrap the content of the cell, similar to a layout in Rails views.
32
+ def with_content_wrapper(component)
33
+ @content_wrapper = component
34
+
35
+ self
36
+ end
37
+
38
+ def call
39
+ content = if content?
40
+ self.content
41
+ elsif @row.header?
42
+ label
43
+ else
44
+ rendered_value
45
+ end
46
+
47
+ content = @content_wrapper.with_content(content).render_in(view_context) if @content_wrapper
48
+
49
+ concat(content_tag(cell_tag, content, **html_attributes))
50
+ end
51
+
52
+ # Return the rendered and sanitized label for the column.
53
+ def label
54
+ @label&.to_s
55
+ end
56
+
57
+ # Return the raw value of the cell (i.e. the value of the data read from the record)
58
+ def value
59
+ @data&.value
60
+ end
61
+
62
+ # Return the serialized and sanitised data value for rendering in the cell.
63
+ def rendered_value
64
+ @data&.to_s
65
+ end
66
+
67
+ # Serialize data for use in blocks, i.e.
68
+ # row.text(:name) { |cell| tag.span(cell) }
69
+ def to_s
70
+ # Note, this can't be `content` because the block is evaluated in order to produce content.
71
+ rendered_value
72
+ end
73
+
74
+ def inspect
75
+ "#<#{self.class.name} method: #{@method.inspect}>"
76
+ end
77
+
78
+ private
79
+
80
+ def cell_tag
81
+ heading? ? :th : :td
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ module Cells
6
+ # Shows Yes/No for boolean values
7
+ class BooleanComponent < CellComponent
8
+ def rendered_value
9
+ value ? "Yes" : "No"
10
+ end
11
+
12
+ private
13
+
14
+ def default_html_attributes
15
+ { class: "type-boolean" }
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ module Cells
6
+ # Formats the value as a money value
7
+ #
8
+ # The value is expected to be in cents.
9
+ # Adds a class to the cell to allow for custom styling
10
+ class CurrencyComponent < CellComponent
11
+ def initialize(options:, **)
12
+ super(**)
13
+
14
+ @options = options
15
+ end
16
+
17
+ def rendered_value
18
+ value.present? ? number_to_currency(value / 100.0, @options) : ""
19
+ end
20
+
21
+ private
22
+
23
+ def default_html_attributes
24
+ { class: "type-currency" }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ module Cells
6
+ # Formats the value as a date
7
+ # @param format [String] date format
8
+ # @param relative [Boolean] if true, the date may be(if within 5 days) shown as a relative date
9
+ class DateComponent < CellComponent
10
+ def initialize(format:, relative:, **)
11
+ super(**)
12
+
13
+ @format = format
14
+ @relative = relative
15
+ end
16
+
17
+ def value
18
+ super&.to_date
19
+ end
20
+
21
+ def rendered_value
22
+ @relative ? relative_time : absolute_time
23
+ end
24
+
25
+ private
26
+
27
+ def default_html_attributes
28
+ {
29
+ class: "type-date",
30
+ title: (absolute_time if row.body? && @relative && value.present? && days_ago_in_words(value).present?),
31
+ }
32
+ end
33
+
34
+ def absolute_time
35
+ value.present? ? I18n.l(value, format: @format) : ""
36
+ end
37
+
38
+ def relative_time
39
+ if value.blank?
40
+ ""
41
+ else
42
+ days_ago_in_words(value)&.capitalize || absolute_time
43
+ end
44
+ end
45
+
46
+ def days_ago_in_words(value)
47
+ from_time = value.to_time
48
+ to_time = Date.current.to_time
49
+ distance_in_days = ((to_time - from_time) / (24.0 * 60.0 * 60.0)).round
50
+
51
+ case distance_in_days
52
+ when 0
53
+ "today"
54
+ when 1
55
+ "yesterday"
56
+ when -1
57
+ "tomorrow"
58
+ when 2..5
59
+ "#{distance_in_days} days ago"
60
+ when -5..-2
61
+ "#{distance_in_days.abs} days from now"
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ module Cells
6
+ # Formats the value as a datetime
7
+ # @param format [String] datetime format
8
+ # @param relative [Boolean] if true, the datetime may be(if today) shown as a relative date/time
9
+ class DateTimeComponent < CellComponent
10
+ include ActionView::Helpers::DateHelper
11
+
12
+ def initialize(format:, relative:, **)
13
+ super(**)
14
+
15
+ @format = format
16
+ @relative = relative
17
+ end
18
+
19
+ def value
20
+ super&.to_datetime
21
+ end
22
+
23
+ def rendered_value
24
+ @relative ? relative_time : absolute_time
25
+ end
26
+
27
+ private
28
+
29
+ def default_html_attributes
30
+ {
31
+ class: "type-datetime",
32
+ title: (absolute_time if row.body? && @relative && today?),
33
+ }
34
+ end
35
+
36
+ def absolute_time
37
+ value.present? ? I18n.l(value, format: @format) : ""
38
+ end
39
+
40
+ def today?
41
+ value&.to_date == Date.current
42
+ end
43
+
44
+ def relative_time
45
+ return "" if value.blank?
46
+
47
+ if today?
48
+ if value > DateTime.current
49
+ "#{distance_of_time_in_words(value, DateTime.current)} from now".capitalize
50
+ else
51
+ "#{distance_of_time_in_words(value, DateTime.current)} ago".capitalize
52
+ end
53
+ else
54
+ absolute_time
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ module Cells
6
+ # Formats the value as a number
7
+ #
8
+ # Adds a class to the cell to allow for custom styling
9
+ class NumberComponent < CellComponent
10
+ def rendered_value
11
+ value.present? ? number_to_human(value) : ""
12
+ end
13
+
14
+ private
15
+
16
+ def default_html_attributes
17
+ { class: "type-number" }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ module Cells
6
+ class OrdinalComponent < CellComponent
7
+ def initialize(primary_key:, **)
8
+ super(**)
9
+
10
+ @primary_key = primary_key
11
+ end
12
+
13
+ def rendered_value
14
+ t("katalyst.tables.orderable.value")
15
+ end
16
+
17
+ private
18
+
19
+ def default_html_attributes
20
+ if @row.header?
21
+ { class: "ordinal" }
22
+ else
23
+ {
24
+ class: "ordinal",
25
+ data: {
26
+ controller: Orderable::ITEM_CONTROLLER,
27
+ "#{Orderable::ITEM_CONTROLLER}-params-value": params.to_json,
28
+ },
29
+ }
30
+ end
31
+ end
32
+
33
+ def params
34
+ {
35
+ id_name: @primary_key,
36
+ id_value: record.public_send(@primary_key),
37
+ index_name: column,
38
+ index_value: record.public_send(column),
39
+ }
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ module Cells
6
+ # Displays the plain text for rich text content
7
+ #
8
+ # Adds a title attribute to allow for hover over display of the full content
9
+ class RichTextComponent < CellComponent
10
+ private
11
+
12
+ def default_html_attributes
13
+ {
14
+ class: "type-rich-text",
15
+ title: (value.to_plain_text unless row.header?),
16
+ }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ module Cells
6
+ class SelectComponent < CellComponent
7
+ def initialize(params:, form_id:, **)
8
+ super(**)
9
+
10
+ @params = params
11
+ @form_id = form_id
12
+ end
13
+
14
+ def rendered_value
15
+ tag.input(type: :checkbox)
16
+ end
17
+
18
+ private
19
+
20
+ def default_html_attributes
21
+ if @row.header?
22
+ { class: "selection" }
23
+ else
24
+ {
25
+ class: "selection",
26
+ data: {
27
+ controller: Selectable::ITEM_CONTROLLER,
28
+ "#{Selectable::ITEM_CONTROLLER}-params-value" => @params.to_json,
29
+ "#{Selectable::ITEM_CONTROLLER}-#{Selectable::FORM_CONTROLLER}-outlet" => "##{@form_id}",
30
+ action: "change->#{Selectable::ITEM_CONTROLLER}#change",
31
+ turbo_permanent: "",
32
+ },
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ class Data
6
+ def initialize(record:, column:)
7
+ @record = record
8
+ @column = column
9
+ end
10
+
11
+ def value
12
+ return @value if defined?(@value)
13
+
14
+ @value = @record&.public_send(@column)
15
+ end
16
+
17
+ def call
18
+ ActionView::OutputBuffer.new.tap do |output|
19
+ output << value.to_s
20
+ end.to_s
21
+ end
22
+
23
+ alias to_s call
24
+
25
+ def inspect
26
+ "#<#{self.class.name} column: #{@column.inspect}, value: #{value.inspect}>"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -24,7 +24,7 @@ module Katalyst
24
24
  end
25
25
 
26
26
  def plural_human_model_name
27
- human = @table.model_name&.human || @table.object_name.to_s.humanize
27
+ human = @table.model_name&.human || @table.object_name&.to_s&.humanize || "record"
28
28
  human.pluralize.downcase
29
29
  end
30
30
 
@@ -0,0 +1,5 @@
1
+ <%= tag.tr(**html_attributes) do %>
2
+ <% cells.each do |cell| %>
3
+ <%= cell %>
4
+ <% end %>
5
+ <% end %>
@@ -5,27 +5,10 @@ module Katalyst
5
5
  class HeaderRowComponent < ViewComponent::Base # :nodoc:
6
6
  include Katalyst::HtmlAttributes
7
7
 
8
- renders_many :columns, ->(component) { component }
8
+ renders_many :cells, ->(cell) { cell }
9
9
 
10
- def initialize(table, link: {})
11
- super()
12
-
13
- @table = table
14
- @link_attributes = link
15
- end
16
-
17
- def call
18
- content # generate content before rendering
19
-
20
- tag.tr(**html_attributes) do
21
- columns.each do |column|
22
- concat(column.to_s)
23
- end
24
- end
25
- end
26
-
27
- def cell(attribute, **, &)
28
- with_column(@table.header_cell_component.new(@table, attribute, link: @link_attributes, **), &)
10
+ def before_render
11
+ content # ensure content is rendered so html_attributes can be set
29
12
  end
30
13
 
31
14
  def header?
@@ -37,11 +20,8 @@ module Katalyst
37
20
  end
38
21
 
39
22
  def inspect
40
- "#<#{self.class.name} link_attributes: #{@link_attributes.inspect}>"
23
+ "#<#{self.class.name}>"
41
24
  end
42
-
43
- # Backwards compatibility with tables 1.0
44
- alias_method :options, :html_attributes=
45
25
  end
46
26
  end
47
27
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ class Label
6
+ def initialize(collection:, column:, label: nil)
7
+ @collection = collection
8
+ @column = column
9
+ @label = label
10
+ end
11
+
12
+ def value
13
+ return @value if defined?(@value)
14
+
15
+ @value = if !@label.nil?
16
+ @label
17
+ elsif @collection.model.present?
18
+ @collection.model.human_attribute_name(@column)
19
+ else
20
+ @column.to_s.humanize.capitalize
21
+ end
22
+ end
23
+
24
+ def call
25
+ ActionView::OutputBuffer.new.tap do |output|
26
+ output << value.to_s
27
+ end.to_s
28
+ end
29
+
30
+ alias to_s call
31
+
32
+ def inspect
33
+ "#<#{self.class.name} column: #{@column.inspect} value: #{value.inspect}>"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ module Orderable
6
+ class FormComponent < ViewComponent::Base # :nodoc:
7
+ include Katalyst::Tables::Identifiable::Defaults
8
+
9
+ attr_reader :id, :url
10
+
11
+ # @param collection [Katalyst::Tables::Collection::Core] the collection to render
12
+ # @param url [String] the url to submit the form to (e.g. <resources>_order_path)
13
+ # @param id [String] the id of the form element (defaults to <resources>_order_form)
14
+ # @param scope [String] the base scope to use for form inputs (defaults to order[<resources>])
15
+ def initialize(collection:, url:, id: nil, scope: nil)
16
+ super
17
+
18
+ @id = id || Orderable.default_form_id(collection)
19
+ @url = url
20
+ @scope = scope || Orderable.default_scope(collection)
21
+ end
22
+
23
+ def call
24
+ form_with(id:, url:, method: :patch, data: {
25
+ controller: FORM_CONTROLLER,
26
+ "#{FORM_CONTROLLER}-scope-value": @scope,
27
+ }) do |form|
28
+ form.button(hidden: "")
29
+ end
30
+ end
31
+
32
+ def inspect
33
+ "#<#{self.class.name} id: #{id.inspect}, url: #{url.inspect}>"
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,12 +1,14 @@
1
1
  <%= form_with(method: :patch,
2
2
  id:,
3
3
  class: "tables--selection--form",
4
- data: { controller: form_controller },
5
- html: { hidden: "" }) do |form| %>
4
+ data: { controller: form_controller,
5
+ turbo_action: "replace",
6
+ turbo_permanent: "" },
7
+ html: { action: false, hidden: "" }) do |form| %>
6
8
  <p class="tables--selection--summary">
7
9
  <span data-<%= form_target("count") %>>0</span>
8
- <span data-<%= form_target("singular") %> hidden><%= @table.collection.model_name.singular %></span>
9
- <span data-<%= form_target("plural") %>><%= @table.collection.model_name.plural %></span>
10
+ <span data-<%= form_target("singular") %> hidden><%= @collection.model_name.singular %></span>
11
+ <span data-<%= form_target("plural") %>><%= @collection.model_name.plural %></span>
10
12
  selected
11
13
  </p>
12
14
  <%= content %>
@@ -4,24 +4,21 @@ module Katalyst
4
4
  module Tables
5
5
  module Selectable
6
6
  class FormComponent < ViewComponent::Base # :nodoc:
7
+ include Katalyst::Tables::Identifiable::Defaults
8
+
7
9
  attr_reader :id, :primary_key
8
10
 
9
- def initialize(table:,
11
+ # @param collection [Katalyst::Tables::Collection::Core] the collection to render
12
+ # @param id [String] the id of the form element (defaults to <resources>_selection_form)
13
+ # @param primary_key [String] the primary key of the record in the collection (defaults to :id)
14
+ def initialize(collection:,
10
15
  id: nil,
11
16
  primary_key: :id)
12
17
  super
13
18
 
14
- @table = table
15
- @id = id
19
+ @collection = collection
20
+ @id = id || Selectable.default_form_id(collection)
16
21
  @primary_key = primary_key
17
-
18
- if @id.nil?
19
- table_id = table.try(:id)
20
-
21
- raise ArgumentError, "Table selection requires an id" if table_id.nil?
22
-
23
- @id = "#{table_id}_selection"
24
- end
25
22
  end
26
23
 
27
24
  def inspect
@@ -2,35 +2,10 @@
2
2
 
3
3
  module Katalyst
4
4
  module Tables
5
- # Utilities for controllers that are generating collections for visualisation
6
- # in a table view using Katalyst::Tables::Frontend.
7
- #
8
- # Provides `table_sort` for sorting based on column interactions (sort param).
5
+ # Configuration for controllers to specify which TableComponent should be used in associated views.
9
6
  module Backend
10
7
  extend ActiveSupport::Concern
11
8
 
12
- # @deprecated backwards compatibility
13
- class SortForm < Katalyst::Tables::Collection::SortForm
14
- end
15
-
16
- # Sort the given collection by params[:sort], which is set when a user
17
- # interacts with a column header in a frontend table view.
18
- #
19
- # @return [[SortForm, ActiveRecord::Relation]]
20
- def table_sort(collection)
21
- column, direction = params[:sort]&.split
22
- direction = "asc" unless SortForm::DIRECTIONS.include?(direction)
23
-
24
- SortForm.new(column:,
25
- direction:)
26
- .apply(collection)
27
- end
28
-
29
- def self_referred?
30
- request.referer.present? && URI.parse(request.referer).path == request.path
31
- end
32
- alias self_refered? self_referred?
33
-
34
9
  included do
35
10
  class_attribute :_default_table_component, instance_accessor: false
36
11
  end
@@ -39,8 +14,7 @@ module Katalyst
39
14
  # Set the table component to be used as the default for all tables
40
15
  # in the views rendered by this controller and its subclasses.
41
16
  #
42
- # ==== Parameters
43
- # * <tt>component</tt> - Default table component, an instance of +Katalyst::TableComponent+
17
+ # @param component [Class] the table component class to use
44
18
  def default_table_component(component)
45
19
  self._default_table_component = component
46
20
  end