katalyst-tables 2.6.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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