katalyst-tables 2.6.0 → 3.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +6 -1
  3. data/README.md +1 -23
  4. data/app/assets/stylesheets/katalyst/tables/_index.scss +1 -0
  5. data/app/assets/stylesheets/katalyst/tables/_ordinal.scss +38 -0
  6. data/app/assets/stylesheets/katalyst/tables/_table.scss +123 -0
  7. data/app/assets/stylesheets/katalyst/tables/typed-columns/_boolean.scss +4 -0
  8. data/app/assets/stylesheets/katalyst/tables/typed-columns/_currency.scss +5 -0
  9. data/app/assets/stylesheets/katalyst/tables/typed-columns/_date.scss +4 -0
  10. data/app/assets/stylesheets/katalyst/tables/typed-columns/_datetime.scss +4 -0
  11. data/app/assets/stylesheets/katalyst/tables/typed-columns/_index.scss +5 -0
  12. data/app/assets/stylesheets/katalyst/tables/typed-columns/_number.scss +5 -0
  13. data/app/components/concerns/katalyst/tables/body/typed_columns.rb +132 -0
  14. data/app/components/concerns/katalyst/tables/header/typed_columns.rb +179 -0
  15. data/app/components/concerns/katalyst/tables/selectable.rb +1 -0
  16. data/app/components/katalyst/table_component.rb +13 -1
  17. data/app/components/katalyst/tables/body/attachment_component.rb +58 -0
  18. data/app/components/katalyst/tables/body/boolean_component.rb +14 -0
  19. data/app/components/katalyst/tables/body/currency_component.rb +29 -0
  20. data/app/components/katalyst/tables/body/date_component.rb +64 -0
  21. data/app/components/katalyst/tables/body/date_time_component.rb +57 -0
  22. data/app/components/katalyst/tables/body/link_component.rb +40 -0
  23. data/app/components/katalyst/tables/body/number_component.rb +22 -0
  24. data/app/components/katalyst/tables/body/rich_text_component.rb +18 -0
  25. data/app/components/katalyst/tables/body_cell_component.rb +9 -1
  26. data/app/components/katalyst/tables/body_row_component.rb +7 -0
  27. data/app/components/katalyst/tables/empty_caption_component.rb +1 -1
  28. data/app/components/katalyst/tables/header/attachment_component.rb +15 -0
  29. data/app/components/katalyst/tables/header/boolean_component.rb +15 -0
  30. data/app/components/katalyst/tables/header/currency_component.rb +15 -0
  31. data/app/components/katalyst/tables/header/date_component.rb +15 -0
  32. data/app/components/katalyst/tables/header/date_time_component.rb +15 -0
  33. data/app/components/katalyst/tables/header/link_component.rb +15 -0
  34. data/app/components/katalyst/tables/header/number_component.rb +15 -0
  35. data/app/components/katalyst/tables/header/rich_text_component.rb +15 -0
  36. data/app/components/katalyst/tables/header_cell_component.rb +35 -3
  37. data/app/components/katalyst/tables/header_row_component.rb +1 -0
  38. data/app/components/katalyst/tables/selectable/form_component.html.erb +3 -1
  39. data/app/models/concerns/katalyst/tables/collection/pagination.rb +8 -1
  40. data/app/models/concerns/katalyst/tables/collection/sorting.rb +3 -3
  41. data/app/models/katalyst/tables/collection/sort_form.rb +26 -8
  42. data/config/locales/tables.en.yml +6 -0
  43. metadata +30 -6
  44. data/app/components/concerns/katalyst/tables/turbo_replaceable.rb +0 -79
  45. data/app/components/katalyst/turbo/pagy_nav_component.rb +0 -23
  46. data/app/components/katalyst/turbo/table_component.rb +0 -45
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ using Katalyst::HtmlAttributes::HasHtmlAttributes
4
+
5
+ module Katalyst
6
+ module Tables
7
+ module Body
8
+ # Shows an attachment
9
+ #
10
+ # The value is expected to be an ActiveStorage attachment
11
+ #
12
+ # If it is representable, shows as a image tag using a default variant named :thumb.
13
+ #
14
+ # Otherwise shows as a link to download.
15
+ class AttachmentComponent < BodyCellComponent
16
+ def initialize(table, record, attribute, variant: :thumb, **options)
17
+ super(table, record, attribute, **options)
18
+
19
+ @variant = variant
20
+ end
21
+
22
+ def rendered_value
23
+ representation
24
+ end
25
+
26
+ def representation
27
+ if value.try(:variable?) && named_variant.present?
28
+ image_tag(value.variant(@variant))
29
+ elsif value.try(:attached?)
30
+ filename.to_s
31
+ else
32
+ ""
33
+ end
34
+ end
35
+
36
+ def filename
37
+ value.blob.filename
38
+ end
39
+
40
+ # Utility for accessing the path Rails provides for retrieving the
41
+ # attachment for use in cells. Example:
42
+ # <% row.attachment :file do |cell| %>
43
+ # <%= link_to "Download", cell.internal_path %>
44
+ # <% end %>
45
+ def internal_path
46
+ rails_blob_path(value, disposition: :attachment)
47
+ end
48
+
49
+ private
50
+
51
+ # Find the reflective variant by name (i.e. :thumb by default)
52
+ def named_variant
53
+ object.attachment_reflections[@attribute.to_s].named_variants[@variant.to_sym]
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ module Body
6
+ # Shows Yes/No for boolean values
7
+ class BooleanComponent < BodyCellComponent
8
+ def rendered_value
9
+ value ? "Yes" : "No"
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ using Katalyst::HtmlAttributes::HasHtmlAttributes
4
+
5
+ module Katalyst
6
+ module Tables
7
+ module Body
8
+ # Formats the value as a money value
9
+ #
10
+ # The value is expected to be in cents.
11
+ # Adds a class to the cell to allow for custom styling
12
+ class CurrencyComponent < BodyCellComponent
13
+ def initialize(table, record, attribute, options: {}, **html_attributes)
14
+ super(table, record, attribute, **html_attributes)
15
+
16
+ @options = options
17
+ end
18
+
19
+ def rendered_value
20
+ value.present? ? number_to_currency(value / 100.0, @options) : ""
21
+ end
22
+
23
+ def default_html_attributes
24
+ super.merge_html(class: "type-currency")
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ module Body
6
+ # Formats the value as a date
7
+ # @param format [String] date format, defaults to :table
8
+ # @param relative [Boolean] if true, the date may be(if within 5 days) shown as a relative date
9
+ class DateComponent < BodyCellComponent
10
+ def initialize(table, record, attribute, format: :table, relative: true, **options)
11
+ super(table, record, attribute, **options)
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 absolute_time
28
+ value.present? ? I18n.l(value, format: @format) : ""
29
+ end
30
+
31
+ def relative_time
32
+ if value.blank?
33
+ ""
34
+ else
35
+ days_ago_in_words(value)&.capitalize || absolute_time
36
+ end
37
+ end
38
+
39
+ def default_html_attributes
40
+ @relative && value.present? && days_ago_in_words(value).present? ? { title: absolute_time } : {}
41
+ end
42
+
43
+ def days_ago_in_words(value)
44
+ from_time = value.to_time
45
+ to_time = Date.current.to_time
46
+ distance_in_days = ((to_time - from_time) / (24.0 * 60.0 * 60.0)).round
47
+
48
+ case distance_in_days
49
+ when 0
50
+ "today"
51
+ when 1
52
+ "yesterday"
53
+ when -1
54
+ "tomorrow"
55
+ when 2..5
56
+ "#{distance_in_days} days ago"
57
+ when -5..-2
58
+ "#{distance_in_days.abs} days from now"
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Katalyst
4
+ module Tables
5
+ module Body
6
+ # Formats the value as a datetime
7
+ # @param format [String] datetime format, defaults to :admin
8
+ # @param relative [Boolean] if true, the datetime may be(if today) shown as a relative date/time
9
+ class DateTimeComponent < BodyCellComponent
10
+ include ActionView::Helpers::DateHelper
11
+
12
+ def initialize(table, record, attribute, format: :table, relative: true, **options)
13
+ super(table, record, attribute, **options)
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 absolute_time
30
+ value.present? ? I18n.l(value, format: @format) : ""
31
+ end
32
+
33
+ def today?
34
+ value&.to_date == Date.current
35
+ end
36
+
37
+ def relative_time
38
+ return "" if value.blank?
39
+
40
+ if today?
41
+ if value > DateTime.current
42
+ "#{distance_of_time_in_words(value, DateTime.current)} from now".capitalize
43
+ else
44
+ "#{distance_of_time_in_words(value, DateTime.current)} ago".capitalize
45
+ end
46
+ else
47
+ absolute_time
48
+ end
49
+ end
50
+
51
+ def default_html_attributes
52
+ @relative && today? ? { title: absolute_time } : {}
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ using Katalyst::HtmlAttributes::HasHtmlAttributes
4
+
5
+ module Katalyst
6
+ module Tables
7
+ module Body
8
+ # Displays a link to the record
9
+ # The link text is the value of the attribute
10
+ # @see Koi::Tables::BodyRowComponent#link
11
+ class LinkComponent < BodyCellComponent
12
+ def initialize(table, record, attribute, url:, link: {}, **options)
13
+ super(table, record, attribute, **options)
14
+
15
+ @url = url
16
+ @link_options = link
17
+ end
18
+
19
+ def call
20
+ content # ensure content is set before rendering options
21
+
22
+ link = content.present? && url.present? ? link_to(content, url, @link_options) : content.to_s
23
+ content_tag(@type, link, **html_attributes)
24
+ end
25
+
26
+ def url
27
+ case @url
28
+ when Symbol
29
+ # helpers are not available until the component is rendered
30
+ @url = helpers.public_send(@url, record)
31
+ when Proc
32
+ @url = @url.call(record)
33
+ else
34
+ @url
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ using Katalyst::HtmlAttributes::HasHtmlAttributes
4
+
5
+ module Katalyst
6
+ module Tables
7
+ module Body
8
+ # Formats the value as a number
9
+ #
10
+ # Adds a class to the cell to allow for custom styling
11
+ class NumberComponent < BodyCellComponent
12
+ def rendered_value
13
+ value.present? ? number_to_human(value) : ""
14
+ end
15
+
16
+ def default_html_attributes
17
+ super.merge_html(class: "type-number")
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ using Katalyst::HtmlAttributes::HasHtmlAttributes
4
+
5
+ module Katalyst
6
+ module Tables
7
+ module Body
8
+ # Displays the plain text for rich text content
9
+ #
10
+ # Adds a title attribute to allow for hover over display of the full content
11
+ class RichTextComponent < BodyCellComponent
12
+ def default_html_attributes
13
+ { title: value.to_plain_text }
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -18,7 +18,7 @@ module Katalyst
18
18
 
19
19
  def before_render
20
20
  # fallback if no content block is given
21
- with_content(value.to_s) unless content?
21
+ with_content(rendered_value) unless content?
22
22
  end
23
23
 
24
24
  def call
@@ -36,6 +36,14 @@ module Katalyst
36
36
  @record.public_send(@attribute)
37
37
  end
38
38
 
39
+ def rendered_value
40
+ value.to_s
41
+ end
42
+
43
+ def to_s
44
+ value.to_s
45
+ end
46
+
39
47
  def inspect
40
48
  "#<#{self.class.name} attribute: #{@attribute.inspect}, value: #{value.inspect}>"
41
49
  end
@@ -4,6 +4,7 @@ module Katalyst
4
4
  module Tables
5
5
  class BodyRowComponent < ViewComponent::Base # :nodoc:
6
6
  include Katalyst::HtmlAttributes
7
+ include Body::TypedColumns
7
8
 
8
9
  renders_many :columns, ->(component) { component }
9
10
 
@@ -36,6 +37,12 @@ module Katalyst
36
37
  true
37
38
  end
38
39
 
40
+ def default_html_attributes
41
+ return {} unless @table.generate_ids?
42
+
43
+ { id: dom_id(@record) }
44
+ end
45
+
39
46
  def inspect
40
47
  "#<#{self.class.name} record: #{record.inspect}>"
41
48
  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,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ using Katalyst::HtmlAttributes::HasHtmlAttributes
4
+
5
+ module Katalyst
6
+ module Tables
7
+ module Header
8
+ class AttachmentComponent < HeaderCellComponent
9
+ def default_html_attributes
10
+ super.merge_html(class: "type-attachment")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ using Katalyst::HtmlAttributes::HasHtmlAttributes
4
+
5
+ module Katalyst
6
+ module Tables
7
+ module Header
8
+ class BooleanComponent < HeaderCellComponent
9
+ def default_html_attributes
10
+ super.merge_html(class: "type-boolean")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ using Katalyst::HtmlAttributes::HasHtmlAttributes
4
+
5
+ module Katalyst
6
+ module Tables
7
+ module Header
8
+ class CurrencyComponent < HeaderCellComponent
9
+ def default_html_attributes
10
+ super.merge_html(class: "type-currency")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ using Katalyst::HtmlAttributes::HasHtmlAttributes
4
+
5
+ module Katalyst
6
+ module Tables
7
+ module Header
8
+ class DateComponent < HeaderCellComponent
9
+ def default_html_attributes
10
+ super.merge_html(class: "type-date")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ using Katalyst::HtmlAttributes::HasHtmlAttributes
4
+
5
+ module Katalyst
6
+ module Tables
7
+ module Header
8
+ class DateTimeComponent < HeaderCellComponent
9
+ def default_html_attributes
10
+ super.merge_html(class: "type-datetime")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ using Katalyst::HtmlAttributes::HasHtmlAttributes
4
+
5
+ module Katalyst
6
+ module Tables
7
+ module Header
8
+ class LinkComponent < HeaderCellComponent
9
+ def default_html_attributes
10
+ super.merge_html(class: "type-link")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ using Katalyst::HtmlAttributes::HasHtmlAttributes
4
+
5
+ module Katalyst
6
+ module Tables
7
+ module Header
8
+ class NumberComponent < HeaderCellComponent
9
+ def default_html_attributes
10
+ super.merge_html(class: "type-number")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ using Katalyst::HtmlAttributes::HasHtmlAttributes
4
+
5
+ module Katalyst
6
+ module Tables
7
+ module Header
8
+ class RichTextComponent < HeaderCellComponent
9
+ def default_html_attributes
10
+ super.merge_html(class: "type-rich-text")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ using Katalyst::HtmlAttributes::HasHtmlAttributes
4
+
3
5
  module Katalyst
4
6
  module Tables
5
7
  class HeaderCellComponent < ViewComponent::Base # :nodoc:
@@ -9,19 +11,20 @@ module Katalyst
9
11
 
10
12
  delegate :object_name, :collection, :sorting, to: :@table
11
13
 
12
- def initialize(table, attribute, label: nil, link: {}, **html_attributes)
14
+ def initialize(table, attribute, label: nil, link: {}, width: nil, **html_attributes)
13
15
  super(**html_attributes)
14
16
 
15
17
  @table = table
16
18
  @attribute = attribute
17
19
  @value = label
20
+ @width = width
18
21
  @link_attributes = link
19
22
  end
20
23
 
21
24
  def call
22
25
  tag.th(**html_attributes) do
23
26
  if sortable?(@attribute)
24
- link_to(value, sort_url(@attribute), **@link_attributes)
27
+ link_to(value, sort_url(@attribute), **link_attributes)
25
28
  else
26
29
  value
27
30
  end
@@ -47,7 +50,7 @@ module Katalyst
47
50
  end
48
51
 
49
52
  def inspect
50
- "#<#{self.class.name} attribute: #{@attribute.inspect}, value: #{value.inspect}>"
53
+ "#<#{self.class.name} attribute: #{@attribute.inspect}, value: #{@value.inspect}>"
51
54
  end
52
55
 
53
56
  # Backwards compatibility with tables 1.0
@@ -55,7 +58,36 @@ module Katalyst
55
58
 
56
59
  private
57
60
 
61
+ def width_class
62
+ case @width
63
+ when :xs
64
+ "width-xs"
65
+ when :s
66
+ "width-s"
67
+ when :m
68
+ "width-m"
69
+ when :l
70
+ "width-l"
71
+ else
72
+ ""
73
+ end
74
+ end
75
+
76
+ def link_attributes
77
+ { data: { turbo_action: "replace" } }.merge_html(@link_attributes)
78
+ end
79
+
58
80
  def default_html_attributes
81
+ sort_data.merge(width_data)
82
+ end
83
+
84
+ def width_data
85
+ return {} unless @width
86
+
87
+ { class: width_class }
88
+ end
89
+
90
+ def sort_data
59
91
  return {} unless sorting&.supports?(collection, @attribute)
60
92
 
61
93
  { data: { sort: sorting.status(@attribute) } }
@@ -4,6 +4,7 @@ module Katalyst
4
4
  module Tables
5
5
  class HeaderRowComponent < ViewComponent::Base # :nodoc:
6
6
  include Katalyst::HtmlAttributes
7
+ include Header::TypedColumns
7
8
 
8
9
  renders_many :columns, ->(component) { component }
9
10
 
@@ -1,7 +1,9 @@
1
1
  <%= form_with(method: :patch,
2
2
  id:,
3
3
  class: "tables--selection--form",
4
- data: { controller: form_controller },
4
+ data: { controller: form_controller,
5
+ turbo_action: "replace",
6
+ turbo_permanent: "" },
5
7
  html: { hidden: "" }) do |form| %>
6
8
  <p class="tables--selection--summary">
7
9
  <span data-<%= form_target("count") %>>0</span>
@@ -35,7 +35,14 @@ module Katalyst
35
35
  end
36
36
 
37
37
  def paginate_options
38
- @paginate.is_a?(Hash) ? @paginate : {}
38
+ opts = @paginate.is_a?(Hash) ? @paginate : {}
39
+ opts = opts.dup
40
+ opts[:anchor_string] ||= anchor_string
41
+ opts
42
+ end
43
+
44
+ def anchor_string
45
+ "data-turbo-action=\"replace\""
39
46
  end
40
47
 
41
48
  class Paginate # :nodoc:
@@ -22,16 +22,16 @@ module Katalyst
22
22
  end
23
23
 
24
24
  def initialize(sorting: config.sorting, **options)
25
- @sorting = SortForm.parse(sorting) if sorting
25
+ @sorting = SortForm.parse(sorting, default: sorting) if sorting
26
26
 
27
- super(sort: sorting, **options) # set default sort based on config
27
+ super(sort: @sorting.to_param, **options) # set default sort based on config
28
28
  end
29
29
 
30
30
  def sort=(value)
31
31
  return unless @sorting
32
32
 
33
33
  # update internal proxy
34
- @sorting = SortForm.parse(value, default: attribute_was(:sort))
34
+ @sorting = SortForm.parse(value, default: @sorting.default)
35
35
 
36
36
  # update attribute based on normalized value
37
37
  super(@sorting.to_param)
@@ -10,25 +10,43 @@ module Katalyst
10
10
 
11
11
  attr_accessor :column, :direction, :default
12
12
 
13
- def self.parse(param, default: nil)
14
- column, direction = param.to_s.split
15
- direction = "asc" unless DIRECTIONS.include?(direction)
16
-
17
- default = SortForm.parse(default).to_param if default.present?
13
+ def self.normalize(param)
14
+ new(param:).to_param
15
+ end
18
16
 
19
- SortForm.new(column:, direction:, default:)
17
+ def self.parse(param, **args)
18
+ new(param:, **args)
20
19
  end
21
20
 
22
- def initialize(column: nil, direction: nil, default: nil)
21
+ def initialize(param: nil, column: nil, direction: nil, default: nil)
22
+ if param.present?
23
+ column, direction = param.to_s.split
24
+ direction = "asc" unless DIRECTIONS.include?(direction)
25
+ end
26
+
23
27
  self.column = column
24
28
  self.direction = direction
25
- self.default = default
29
+ self.default = SortForm.normalize(default) if default
26
30
  end
27
31
 
28
32
  def to_param
29
33
  "#{column} #{direction}"
30
34
  end
31
35
 
36
+ def default?
37
+ to_param == default.to_param
38
+ end
39
+
40
+ def hash
41
+ to_param.hash
42
+ end
43
+
44
+ def eql?(other)
45
+ to_param == other.to_param
46
+ end
47
+
48
+ alias to_s to_param
49
+
32
50
  # Returns true if the given collection supports sorting on the given
33
51
  # column. A column supports sorting if it is a database column or if
34
52
  # the collection responds to `order_by_#{column}(direction)`.