katalyst-koi 4.5.0.beta.2 → 4.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,11 +131,12 @@ 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
142
  def default_html_attributes
@@ -73,33 +146,43 @@ module Koi
73
146
 
74
147
  # Displays a link to the record
75
148
  # The link text is the value of the attribute
76
- # @param url [Proc] a proc that takes the record and returns the url, defaults to [:admin, record]
149
+ # @see Koi::Tables::BodyRowComponent#link
77
150
  class LinkComponent < BodyCellComponent
78
- def initialize(table, record, attribute, url: ->(object) { [:admin, object] }, **attributes)
79
- super(table, record, attribute, **attributes)
151
+ def initialize(table, record, attribute, url:, link: {}, **options)
152
+ super(table, record, attribute, **options)
80
153
 
81
- @url_builder = url
154
+ @url = url
155
+ @link_options = link
82
156
  end
83
157
 
84
158
  def call
85
159
  content # ensure content is set before rendering options
86
160
 
87
- link = content.present? && url.present? ? link_to(content, url) : content.to_s
161
+ link = content.present? && url.present? ? link_to(content, url, @link_options) : content.to_s
88
162
  content_tag(@type, link, **html_attributes)
89
163
  end
90
164
 
91
165
  def url
92
- @url ||= if @url_builder.is_a?(Symbol)
93
- helpers.public_send(@url_builder, record)
94
- else
95
- @url_builder.call(record)
96
- 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
97
175
  end
98
176
  end
99
177
 
100
- # Shows a thumbnail image
101
- # The value is expected to be an ActiveStorage attachment with a default variant named :thumb
102
- 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
103
186
  def initialize(table, record, attribute, variant: :thumb, **options)
104
187
  super(table, record, attribute, **options)
105
188
 
@@ -107,7 +190,13 @@ module Koi
107
190
  end
108
191
 
109
192
  def rendered_value
110
- 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
111
200
  end
112
201
  end
113
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