katalyst-koi 4.5.0.beta.1 → 4.5.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.
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ using Katalyst::HtmlAttributes::HasHtmlAttributes
4
+
3
5
  module Koi
4
6
  module Tables
5
7
  module Body
@@ -11,47 +13,117 @@ module Koi
11
13
  end
12
14
 
13
15
  # Formats the value as a date
14
- # default format is :admin
16
+ # @param format [String] date format, defaults to :admin
17
+ # @param relative [Boolean] if true, the date may be(if within 5 days) shown as a relative date
15
18
  class DateComponent < BodyCellComponent
16
- def initialize(table, record, attribute, format: :admin, **options)
19
+ include Koi::DateHelper
20
+
21
+ def initialize(table, record, attribute, format: :admin, relative: true, **options)
17
22
  super(table, record, attribute, **options)
18
23
 
19
- @format = format
24
+ @format = format
25
+ @relative = relative
26
+ end
27
+
28
+ def value
29
+ super&.to_date
20
30
  end
21
31
 
22
32
  def rendered_value
23
- value.present? ? l(value.to_date, format: @format) : ""
33
+ @relative ? relative_time : absolute_time
34
+ end
35
+
36
+ private
37
+
38
+ def absolute_time
39
+ value.present? ? I18n.l(value, format: @format) : ""
40
+ end
41
+
42
+ def relative_time
43
+ if value.blank?
44
+ ""
45
+ else
46
+ days_ago_in_words(value)&.capitalize || absolute_time
47
+ end
48
+ end
49
+
50
+ def default_html_attributes
51
+ @relative && value.present? && days_ago_in_words(value).present? ? { title: absolute_time } : {}
24
52
  end
25
53
  end
26
54
 
27
55
  # Formats the value as a datetime
28
- # default format is :admin
56
+ # @param format [String] datetime format, defaults to :admin
57
+ # @param relative [Boolean] if true, the datetime may be(if today) shown as a relative date/time
29
58
  class DatetimeComponent < BodyCellComponent
30
- def initialize(table, record, attribute, format: :admin, **options)
59
+ include ActionView::Helpers::DateHelper
60
+
61
+ def initialize(table, record, attribute, format: :admin, relative: true, **options)
31
62
  super(table, record, attribute, **options)
32
63
 
33
- @format = format
64
+ @format = format
65
+ @relative = relative
66
+ end
67
+
68
+ def value
69
+ super&.to_datetime
34
70
  end
35
71
 
36
72
  def rendered_value
37
- value.present? ? l(value.to_datetime, format: @format) : ""
73
+ @relative ? relative_time : absolute_time
74
+ end
75
+
76
+ private
77
+
78
+ def absolute_time
79
+ value.present? ? I18n.l(value, format: @format) : ""
80
+ end
81
+
82
+ def today?
83
+ value.to_date == Date.current
84
+ end
85
+
86
+ def relative_time
87
+ return "" if value.blank?
88
+
89
+ if today?
90
+ if value > DateTime.current
91
+ "#{distance_of_time_in_words(value, DateTime.current)} from now".capitalize
92
+ else
93
+ "#{distance_of_time_in_words(value, DateTime.current)} ago".capitalize
94
+ end
95
+ else
96
+ absolute_time
97
+ end
98
+ end
99
+
100
+ def default_html_attributes
101
+ @relative && today? ? { title: absolute_time } : {}
38
102
  end
39
103
  end
40
104
 
41
105
  # Formats the value as a money value
42
- # The value is expected to be in cents
106
+ #
107
+ # The value is expected to be in cents.
43
108
  # Adds a class to the cell to allow for custom styling
44
- class MoneyComponent < BodyCellComponent
109
+ class CurrencyComponent < BodyCellComponent
110
+ def initialize(table, record, attribute, options: {}, **html_attributes)
111
+ super(table, record, attribute, **html_attributes)
112
+
113
+ @options = options
114
+ end
115
+
45
116
  def rendered_value
46
- value.present? ? number_to_currency(value / 100.0) : ""
117
+ value.present? ? number_to_currency(value / 100.0, @options) : ""
47
118
  end
48
119
 
49
120
  def default_html_attributes
50
- { class: "number" }
121
+ super.merge_html(class: "type-currency")
51
122
  end
52
123
  end
53
124
 
54
125
  # Formats the value as a number
126
+ #
55
127
  # Adds a class to the cell to allow for custom styling
56
128
  class NumberComponent < BodyCellComponent
57
129
  def rendered_value
@@ -59,51 +131,58 @@ module Koi
59
131
  end
60
132
 
61
133
  def default_html_attributes
62
- { class: "number" }
134
+ super.merge_html(class: "type-number")
63
135
  end
64
136
  end
65
137
 
66
138
  # Displays the plain text for rich text content
139
+ #
67
140
  # Adds a title attribute to allow for hover over display of the full content
68
141
  class RichTextComponent < BodyCellComponent
69
- def rendered_value
70
- value.to_plain_text
71
- end
72
-
73
142
  def default_html_attributes
74
- { title: rendered_value }
143
+ { title: value.to_plain_text }
75
144
  end
76
145
  end
77
146
 
78
147
  # Displays a link to the record
79
148
  # The link text is the value of the attribute
80
- # @param url [Proc] a proc that takes the record and returns the url, defaults to [:admin, record]
149
+ # @see Koi::Tables::BodyRowComponent#link
81
150
  class LinkComponent < BodyCellComponent
82
- def initialize(table, record, attribute, url: ->(object) { [:admin, object] }, **attributes)
83
- super(table, record, attribute, **attributes)
151
+ def initialize(table, record, attribute, url:, link: {}, **options)
152
+ super(table, record, attribute, **options)
84
153
 
85
- @url_builder = url
154
+ @url = url
155
+ @link_options = link
86
156
  end
87
157
 
88
158
  def call
89
159
  content # ensure content is set before rendering options
90
160
 
91
- link = content.present? && url.present? ? link_to(value, url) : ""
161
+ link = content.present? && url.present? ? link_to(content, url, @link_options) : content.to_s
92
162
  content_tag(@type, link, **html_attributes)
93
163
  end
94
164
 
95
165
  def url
96
- @url ||= if @url_builder.is_a?(Symbol)
97
- helpers.public_send(@url_builder, record)
98
- else
99
- @url_builder.call(record)
100
- end
166
+ case @url
167
+ when Symbol
168
+ # helpers are not available until the component is rendered
169
+ @url = helpers.public_send(@url, record)
170
+ when Proc
171
+ @url = @url.call(record)
172
+ else
173
+ @url
174
+ end
101
175
  end
102
176
  end
103
177
 
104
- # Shows a thumbnail image
105
- # The value is expected to be an ActiveStorage attachment with a variant named :thumb
106
- class ImageComponent < BodyCellComponent
178
+ # Shows an attachment
179
+ #
180
+ # The value is expected to be an ActiveStorage attachment
181
+ #
182
+ # If it is representable, shows as a image tag using a default variant named :thumb.
183
+ #
184
+ # Otherwise shows as a link to download.
185
+ class AttachmentComponent < BodyCellComponent
107
186
  def initialize(table, record, attribute, variant: :thumb, **options)
108
187
  super(table, record, attribute, **options)
109
188
 
@@ -111,7 +190,13 @@ module Koi
111
190
  end
112
191
 
113
192
  def rendered_value
114
- value.present? ? image_tag(value.variant(@variant)) : ""
193
+ if value.try(:representable?)
194
+ image_tag(@variant.nil? ? value : value.variant(@variant))
195
+ elsif value.try(:attached?)
196
+ link_to value.blob.filename, rails_blob_path(value, disposition: :attachment)
197
+ else
198
+ ""
199
+ end
115
200
  end
116
201
  end
117
202
  end
@@ -4,40 +4,138 @@ module Koi
4
4
  module Tables
5
5
  # Custom body row component, in order to override the default body cell component
6
6
  class BodyRowComponent < Katalyst::Tables::BodyRowComponent
7
- def boolean(attribute, **options, &block)
8
- with_column(Body::BooleanComponent.new(@table, @record, attribute, **options), &block)
7
+ # Generates a column from boolean values rendered as "Yes" or "No".
8
+ #
9
+ # @param method [Symbol] the method to call on the record
10
+ # @param attributes [Hash] HTML attributes to be added to the cell
11
+ # @param block [Proc] optional block to alter the cell content
12
+ # @return [void]
13
+ #
14
+ # @example Render a boolean column indicating whether the record is active
15
+ # <% row.boolean :active %> # => <td>Yes</td>
16
+ def boolean(method, **attributes, &block)
17
+ with_column(Body::BooleanComponent.new(@table, @record, method, **attributes), &block)
9
18
  end
10
19
 
11
- def date(attribute, format: :admin, **options, &block)
12
- with_column(Body::DateComponent.new(@table, @record, attribute, format:, **options), &block)
20
+ # Generates a column from date values rendered using I18n.l.
21
+ # The default format is :admin, but it can be overridden.
22
+ #
23
+ # @param method [Symbol] the method to call on the record
24
+ # @param format [Symbol] the I18n date format to use when rendering
25
+ # @param attributes [Hash] HTML attributes to be added to the cell tag
26
+ # @param block [Proc] optional block to alter the cell content
27
+ # @return [void]
28
+ #
29
+ # @example Render a date column describing when the record was created
30
+ # <% row.date :created_at %> # => <td>29 Feb 2024</td>
31
+ def date(method, format: :admin, **attributes, &block)
32
+ with_column(Body::DateComponent.new(@table, @record, method, format:, **attributes), &block)
13
33
  end
14
34
 
15
- def datetime(attribute, format: :admin, **options, &block)
16
- with_column(Body::DatetimeComponent.new(@table, @record, attribute, format:, **options), &block)
35
+ # Generates a column from datetime values rendered using I18n.l.
36
+ # The default format is :admin, but it can be overridden.
37
+ #
38
+ # @param method [Symbol] the method to call on the record
39
+ # @param format [Symbol] the I18n datetime format to use when rendering
40
+ # @param attributes [Hash] HTML attributes to be added to the cell tag
41
+ # @param block [Proc] optional block to alter the cell content
42
+ # @return [void]
43
+ #
44
+ # @example Render a datetime column describing when the record was created
45
+ # <% row.datetime :created_at %> # => <td>29 Feb 2024, 5:00pm</td>
46
+ def datetime(method, format: :admin, **attributes, &block)
47
+ with_column(Body::DatetimeComponent.new(@table, @record, method, format:, **attributes), &block)
17
48
  end
18
49
 
19
- def number(attribute, **options, &block)
20
- with_column(Body::NumberComponent.new(@table, @record, attribute, **options), &block)
50
+ # Generates a column from numeric values formatted appropriately.
51
+ #
52
+ # @param method [Symbol] the method to call on the record
53
+ # @param attributes [Hash] HTML attributes to be added to the cell tag
54
+ # @param block [Proc] optional block to alter the cell content
55
+ # @return [void]
56
+ #
57
+ # @example Render the number of comments on a post
58
+ # <% row.number :comment_count %> # => <td>0</td>
59
+ def number(method, **attributes, &block)
60
+ with_column(Body::NumberComponent.new(@table, @record, method, **attributes), &block)
21
61
  end
22
62
 
23
- def money(attribute, **options, &block)
24
- with_column(Body::MoneyComponent.new(@table, @record, attribute, **options), &block)
63
+ # Generates a column from numeric values rendered using `number_to_currency`.
64
+ #
65
+ # @param method [Symbol] the method to call on the record
66
+ # @param options [Hash] options to be passed to `number_to_currency`
67
+ # @param attributes [Hash] HTML attributes to be added to the cell tag
68
+ # @param block [Proc] optional block to alter the cell content
69
+ # @return [void]
70
+ #
71
+ # @example Render a currency column for the price of a product
72
+ # <% row.currency :price %> # => <td>$3.50</td>
73
+ def currency(method, options: {}, **attributes, &block)
74
+ with_column(Body::CurrencyComponent.new(@table, @record, method, options:, **attributes), &block)
25
75
  end
26
76
 
27
- def rich_text(attribute, **options, &block)
28
- with_column(Body::RichTextComponent.new(@table, @record, attribute, **options), &block)
77
+ # Generates a column containing HTML markup.
78
+ #
79
+ # @param method [Symbol] the method to call on the record
80
+ # @param attributes [Hash] HTML attributes to be added to the cell tag
81
+ # @param block [Proc] optional block to alter the cell content
82
+ # @return [void]
83
+ #
84
+ # @note This method assumes that the method returns HTML-safe content.
85
+ # If the content is not HTML-safe, it will be escaped.
86
+ #
87
+ # @example Render a description column containing HTML markup
88
+ # <% row.rich_text :description %> # => <td><em>Emphasis</em></td>
89
+ def rich_text(method, **attributes, &block)
90
+ with_column(Body::RichTextComponent.new(@table, @record, method, **attributes), &block)
29
91
  end
30
92
 
31
- def link(attribute, **options, &block)
32
- with_column(Body::LinkComponent.new(@table, @record, attribute, **options), &block)
93
+ # Generates a column that links to the record's show page (by default).
94
+ #
95
+ # @param method [Symbol] the method to call on the record
96
+ # @param link [Hash] options to be passed to the link_to helper
97
+ # @option opts [Hash, Array, String, Symbol] :url ([:admin, object]) options for url_for,
98
+ # or a symbol to be passed to the route helper
99
+ # @param attributes [Hash] HTML attributes to be added to the cell tag
100
+ # @param block [Proc] optional block to alter the cell content
101
+ # @return [void]
102
+ #
103
+ # @example Render a column containing the record's title, linked to its show page
104
+ # <% row.link :title %> # => <td><a href="/admin/post/15">About us</a></td>
105
+ # @example Render a column containing the record's title, linked to its edit page
106
+ # <% row.link :title, url: :edit_admin_post_path do |cell| %>
107
+ # Edit <%= cell %>
108
+ # <% end %>
109
+ # # => <td><a href="/admin/post/15/edit">Edit About us</a></td>
110
+ def link(method, url: [:admin, @record], link: {}, **attributes, &block)
111
+ with_column(Body::LinkComponent.new(@table, @record, method, url:, link:, **attributes), &block)
33
112
  end
34
113
 
35
- def text(attribute, **options, &block)
36
- with_column(BodyCellComponent.new(@table, @record, attribute, **options), &block)
114
+ # Generates a column that renders the contents as text.
115
+ #
116
+ # @param method [Symbol] the method to call on the record
117
+ # @param attributes [Hash] HTML attributes to be added to the cell tag
118
+ # @param block [Proc] optional block to alter the cell content
119
+ # @return [void]
120
+ #
121
+ # @example Render a column containing the record's title
122
+ # <% row.text :title %> # => <td>About us</td>
123
+ def text(method, **attributes, &block)
124
+ with_column(BodyCellComponent.new(@table, @record, method, **attributes), &block)
37
125
  end
38
126
 
39
- def image(attribute, variant: :thumb, **options, &block)
40
- with_column(Body::ImageComponent.new(@table, @record, attribute, variant:, **options), &block)
127
+ # Generates a column that renders an ActiveStorage attachment as a downloadable link.
128
+ #
129
+ # @param method [Symbol] the method to call on the record
130
+ # @param variant [Symbol] the variant to use when rendering the image (default :thumb)
131
+ # @param attributes [Hash] HTML attributes to be added to the cell tag
132
+ # @param block [Proc] optional block to alter the cell content
133
+ # @return [void]
134
+ #
135
+ # @example Render a column containing a download link to the record's background image
136
+ # <% row.attachment :background %> # => <td><a href="...">background.png</a></td>
137
+ def attachment(method, variant: :thumb, **attributes, &block)
138
+ with_column(Body::AttachmentComponent.new(@table, @record, method, variant:, **attributes), &block)
41
139
  end
42
140
  end
43
141
  end
@@ -1,11 +1,61 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ using Katalyst::HtmlAttributes::HasHtmlAttributes
4
+
3
5
  module Koi
4
6
  module Tables
5
7
  module Header
6
8
  class NumberComponent < HeaderCellComponent
7
9
  def default_html_attributes
8
- { class: "number" }
10
+ super.merge_html(class: "type-number")
11
+ end
12
+ end
13
+
14
+ class CurrencyComponent < HeaderCellComponent
15
+ def default_html_attributes
16
+ super.merge_html(class: "type-currency")
17
+ end
18
+ end
19
+
20
+ class BooleanComponent < HeaderCellComponent
21
+ def default_html_attributes
22
+ super.merge_html(class: "type-boolean")
23
+ end
24
+ end
25
+
26
+ class DateComponent < HeaderCellComponent
27
+ def default_html_attributes
28
+ super.merge_html(class: "type-date")
29
+ end
30
+ end
31
+
32
+ class DateTimeComponent < HeaderCellComponent
33
+ def default_html_attributes
34
+ super.merge_html(class: "type-datetime")
35
+ end
36
+ end
37
+
38
+ class LinkComponent < HeaderCellComponent
39
+ def default_html_attributes
40
+ super.merge_html(class: "type-link")
41
+ end
42
+ end
43
+
44
+ class TextComponent < HeaderCellComponent
45
+ def default_html_attributes
46
+ super.merge_html(class: "type-text")
47
+ end
48
+ end
49
+
50
+ class ImageComponent < HeaderCellComponent
51
+ def default_html_attributes
52
+ super.merge_html(class: "type-image")
53
+ end
54
+ end
55
+
56
+ class AttachmentComponent < HeaderCellComponent
57
+ def default_html_attributes
58
+ super.merge_html(class: "type-attachment")
9
59
  end
10
60
  end
11
61
  end
@@ -1,8 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ using Katalyst::HtmlAttributes::HasHtmlAttributes
4
+
3
5
  module Koi
4
6
  module Tables
5
7
  class HeaderCellComponent < Katalyst::Tables::HeaderCellComponent
8
+ attr_reader :width
9
+
10
+ def initialize(table, attribute, label: nil, link: {}, width: nil, **html_attributes)
11
+ @width = width
12
+
13
+ super(table, attribute, label:, link:, **html_attributes)
14
+ end
15
+
16
+ def default_html_attributes
17
+ super.merge_html(class: width_class)
18
+ end
19
+
20
+ private
21
+
22
+ def width_class
23
+ case width
24
+ when :xs
25
+ "width-xs"
26
+ when :s
27
+ "width-s"
28
+ when :m
29
+ "width-m"
30
+ when :l
31
+ "width-l"
32
+ else
33
+ ""
34
+ end
35
+ end
6
36
  end
7
37
  end
8
38
  end