katalyst-tables 1.1.0 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +166 -134
- data/app/assets/config/katalyst-tables.js +1 -0
- data/app/assets/javascripts/controllers/tables/turbo_collection_controller.js +22 -0
- data/app/components/concerns/katalyst/tables/configurable_component.rb +31 -0
- data/app/components/concerns/katalyst/tables/has_html_attributes.rb +45 -0
- data/app/components/concerns/katalyst/tables/has_table_content.rb +33 -0
- data/app/components/concerns/katalyst/tables/sortable.rb +32 -0
- data/app/components/concerns/katalyst/tables/turbo_replaceable.rb +62 -0
- data/app/components/katalyst/table_component.rb +102 -0
- data/app/components/katalyst/tables/body_cell_component.rb +40 -0
- data/app/components/katalyst/tables/body_row_component.rb +40 -0
- data/app/components/katalyst/tables/empty_caption_component.html.erb +6 -0
- data/app/components/katalyst/tables/empty_caption_component.rb +38 -0
- data/app/components/katalyst/tables/header_cell_component.rb +58 -0
- data/app/components/katalyst/tables/header_row_component.rb +40 -0
- data/app/components/katalyst/tables/pagy_nav_component.rb +26 -0
- data/app/components/katalyst/turbo/pagy_nav_component.rb +23 -0
- data/app/components/katalyst/turbo/table_component.rb +48 -0
- data/app/models/concerns/katalyst/tables/collection/core.rb +71 -0
- data/app/models/concerns/katalyst/tables/collection/pagination.rb +66 -0
- data/app/models/concerns/katalyst/tables/collection/sorting.rb +63 -0
- data/app/models/katalyst/tables/collection.rb +32 -0
- data/config/importmap.rb +7 -0
- data/lib/katalyst/tables/backend/sort_form.rb +16 -2
- data/lib/katalyst/tables/backend.rb +8 -8
- data/lib/katalyst/tables/engine.rb +24 -0
- data/lib/katalyst/tables/frontend/helper.rb +8 -9
- data/lib/katalyst/tables/frontend.rb +6 -36
- data/lib/katalyst/tables/version.rb +1 -1
- data/lib/katalyst/tables.rb +5 -0
- metadata +54 -9
- data/lib/katalyst/tables/frontend/builder/base.rb +0 -63
- data/lib/katalyst/tables/frontend/builder/body_cell.rb +0 -31
- data/lib/katalyst/tables/frontend/builder/body_row.rb +0 -29
- data/lib/katalyst/tables/frontend/builder/header_cell.rb +0 -55
- data/lib/katalyst/tables/frontend/builder/header_row.rb +0 -23
- data/lib/katalyst/tables/frontend/table_builder.rb +0 -71
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
# Adds support for turbo stream replacement to ViewComponents. Components
|
6
|
+
# that are rendered from a turbo-stream-compatible response will be rendered
|
7
|
+
# using turbo stream replacement. Components must define `id`.
|
8
|
+
#
|
9
|
+
# Turbo stream replacement rendering will only be enabled if the component
|
10
|
+
# passes `turbo: true` as a constructor option.
|
11
|
+
module TurboReplaceable
|
12
|
+
extend ActiveSupport::Concern
|
13
|
+
|
14
|
+
include ::Turbo::StreamsHelper
|
15
|
+
|
16
|
+
def turbo?
|
17
|
+
@turbo
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(turbo: true, **options)
|
21
|
+
super(**options)
|
22
|
+
|
23
|
+
@turbo = turbo
|
24
|
+
end
|
25
|
+
|
26
|
+
class_methods do
|
27
|
+
# Redefine the compiler to use our custom compiler.
|
28
|
+
# Compiler is set on `inherited` so we need to re-set it if it's not the expected type.
|
29
|
+
def compiler
|
30
|
+
@vc_compiler = @vc_compiler.is_a?(TurboCompiler) ? @vc_compiler : TurboCompiler.new(self)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
included do
|
35
|
+
# ensure that our custom compiler is used, as `inherited` calls `compile` before our module is included.
|
36
|
+
compile(force: true) if compiled?
|
37
|
+
end
|
38
|
+
|
39
|
+
# Wraps the default compiler provided by ViewComponent to add turbo support.
|
40
|
+
class TurboCompiler < ViewComponent::Compiler
|
41
|
+
private
|
42
|
+
|
43
|
+
def define_render_template_for # rubocop:disable Metrics/MethodLength
|
44
|
+
super
|
45
|
+
|
46
|
+
redefinition_lock.synchronize do
|
47
|
+
component_class.alias_method(:vc_render_template_for, :render_template_for)
|
48
|
+
component_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
49
|
+
def render_template_for(variant = nil)
|
50
|
+
return vc_render_template_for(variant) unless turbo?
|
51
|
+
controller.respond_to do |format|
|
52
|
+
format.html { vc_render_template_for(variant) }
|
53
|
+
format.turbo_stream { turbo_stream.replace(id, vc_render_template_for(variant)) }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
RUBY
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
# A component for rendering a table from a collection, with a header row.
|
5
|
+
# ```erb
|
6
|
+
# <%= Katalyst::TableComponent.new(collection: @people) do |row, person| %>
|
7
|
+
# <%= row.cell :name do |cell| %>
|
8
|
+
# <%= link_to cell.value, person %>
|
9
|
+
# <% end %>
|
10
|
+
# <%= row.cell :email %>
|
11
|
+
# <% end %>
|
12
|
+
# ```
|
13
|
+
class TableComponent < ViewComponent::Base
|
14
|
+
include Tables::ConfigurableComponent
|
15
|
+
include Tables::HasHtmlAttributes
|
16
|
+
include Tables::HasTableContent
|
17
|
+
|
18
|
+
attr_reader :collection, :object_name
|
19
|
+
|
20
|
+
config_component :header_row, default: "Katalyst::Tables::HeaderRowComponent"
|
21
|
+
config_component :header_cell, default: "Katalyst::Tables::HeaderCellComponent"
|
22
|
+
config_component :body_row, default: "Katalyst::Tables::BodyRowComponent"
|
23
|
+
config_component :body_cell, default: "Katalyst::Tables::BodyCellComponent"
|
24
|
+
config_component :caption, default: "Katalyst::Tables::EmptyCaptionComponent"
|
25
|
+
|
26
|
+
# Construct a new table component. This entry point supports a large number
|
27
|
+
# of options for customizing the table. The most common options are:
|
28
|
+
# - `collection`: the collection to render
|
29
|
+
# - `sorting`: the sorting to apply to the collection (defaults to collection.storing if available)
|
30
|
+
# - `header`: whether to render the header row (defaults to true, supports options)
|
31
|
+
# - `caption`: whether to render the caption (defaults to true, supports options)
|
32
|
+
# - `object_name`: the name of the object to use for partial rendering (defaults to collection.model_name.i18n_key)
|
33
|
+
# - `partial`: the name of the partial to use for rendering each row (defaults to collection.model_name.param_key)
|
34
|
+
# - `as`: the name of the local variable to use for rendering each row (defaults to collection.model_name.param_key)
|
35
|
+
# In addition to these options, standard HTML attributes can be passed which will be added to the table tag.
|
36
|
+
def initialize(collection:,
|
37
|
+
sorting: nil,
|
38
|
+
header: true,
|
39
|
+
caption: false,
|
40
|
+
**html_attributes)
|
41
|
+
@collection = collection
|
42
|
+
|
43
|
+
# sorting: instance of Katalyst::Tables::Backend::SortForm. If not provided will be inferred from the collection.
|
44
|
+
@sorting = sorting || html_attributes.delete(:sort) # backwards compatibility with old `sort` option
|
45
|
+
|
46
|
+
# header: true means render the header row, header: false means no header row, if a hash, passes as options
|
47
|
+
@header = header
|
48
|
+
@header_options = (header if header.is_a?(Hash)) || {}
|
49
|
+
|
50
|
+
# caption: true means render the caption, caption: false means no caption, if a hash, passes as options
|
51
|
+
@caption = caption
|
52
|
+
@caption_options = (caption if caption.is_a?(Hash)) || {}
|
53
|
+
|
54
|
+
super(**html_attributes)
|
55
|
+
end
|
56
|
+
|
57
|
+
def call
|
58
|
+
tag.table(**html_attributes) do
|
59
|
+
concat(caption)
|
60
|
+
concat(thead)
|
61
|
+
concat(tbody)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def caption
|
66
|
+
caption_component&.new(self)&.render_in(view_context) if @caption
|
67
|
+
end
|
68
|
+
|
69
|
+
def thead
|
70
|
+
return "".html_safe unless @header
|
71
|
+
|
72
|
+
tag.thead do
|
73
|
+
concat(render_header)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def tbody
|
78
|
+
tag.tbody do
|
79
|
+
collection.each do |record|
|
80
|
+
concat(render_row(record))
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def render_header
|
86
|
+
# extract the column's block from the slot and pass it to the cell for rendering
|
87
|
+
header_row_component.new(self, **@header_options).render_in(view_context, &row_proc)
|
88
|
+
end
|
89
|
+
|
90
|
+
def render_row(record)
|
91
|
+
body_row_component.new(self, record).render_in(view_context) do |row|
|
92
|
+
row_proc.call(row, record)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def sorting
|
97
|
+
return @sorting if @sorting.present?
|
98
|
+
|
99
|
+
collection.sorting if collection.respond_to?(:sorting)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
class BodyCellComponent < ViewComponent::Base # :nodoc:
|
6
|
+
include HasHtmlAttributes
|
7
|
+
|
8
|
+
attr_reader :record
|
9
|
+
|
10
|
+
def initialize(table, record, attribute, heading: false, **html_attributes)
|
11
|
+
super(**html_attributes)
|
12
|
+
|
13
|
+
@table = table
|
14
|
+
@record = record
|
15
|
+
@attribute = attribute
|
16
|
+
@type = heading ? :th : :td
|
17
|
+
end
|
18
|
+
|
19
|
+
def before_render
|
20
|
+
# fallback if no content block is given
|
21
|
+
with_content(value.to_s) unless content?
|
22
|
+
end
|
23
|
+
|
24
|
+
def call
|
25
|
+
content # ensure content is set before rendering options
|
26
|
+
|
27
|
+
content_tag(@type, content, **html_attributes)
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return the object for this row.
|
31
|
+
def object
|
32
|
+
@record
|
33
|
+
end
|
34
|
+
|
35
|
+
def value
|
36
|
+
@record.public_send(@attribute)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
class BodyRowComponent < ViewComponent::Base # :nodoc:
|
6
|
+
include HasHtmlAttributes
|
7
|
+
|
8
|
+
renders_many :columns, ->(component) { component }
|
9
|
+
|
10
|
+
def initialize(table, record)
|
11
|
+
super()
|
12
|
+
|
13
|
+
@table = table
|
14
|
+
@record = record
|
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, **options, &block)
|
28
|
+
with_column(@table.body_cell_component.new(@table, @record, attribute, **options), &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def header?
|
32
|
+
false
|
33
|
+
end
|
34
|
+
|
35
|
+
def body?
|
36
|
+
true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
class EmptyCaptionComponent < ViewComponent::Base # :nodoc:
|
6
|
+
include HasHtmlAttributes
|
7
|
+
|
8
|
+
def initialize(table, **html_attributes)
|
9
|
+
super(**html_attributes)
|
10
|
+
|
11
|
+
@table = table
|
12
|
+
end
|
13
|
+
|
14
|
+
def render?
|
15
|
+
@table.collection.empty?
|
16
|
+
end
|
17
|
+
|
18
|
+
def filtered?
|
19
|
+
@table.collection.respond_to?(:filtered?) && @table.collection.filtered?
|
20
|
+
end
|
21
|
+
|
22
|
+
def clear_filters_path
|
23
|
+
url_for
|
24
|
+
end
|
25
|
+
|
26
|
+
def plural_human_model_name
|
27
|
+
human = @table.model_name&.human || @table.object_name.to_s.humanize
|
28
|
+
human.pluralize.downcase
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def default_attributes
|
34
|
+
{ align: "bottom" }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
class HeaderCellComponent < ViewComponent::Base # :nodoc:
|
6
|
+
include Frontend::Helper
|
7
|
+
include HasHtmlAttributes
|
8
|
+
include Sortable
|
9
|
+
|
10
|
+
delegate :object_name, :collection, :sorting, to: :@table
|
11
|
+
|
12
|
+
def initialize(table, attribute, label: nil, link: {}, **html_attributes)
|
13
|
+
super(**html_attributes)
|
14
|
+
|
15
|
+
@table = table
|
16
|
+
@attribute = attribute
|
17
|
+
@value = label
|
18
|
+
@link_attributes = link
|
19
|
+
end
|
20
|
+
|
21
|
+
def call
|
22
|
+
tag.th(**html_attributes) do
|
23
|
+
if sortable?(@attribute)
|
24
|
+
link_to(value, sort_url(@attribute), **@link_attributes)
|
25
|
+
else
|
26
|
+
value
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def value
|
32
|
+
if !@value.nil?
|
33
|
+
@value
|
34
|
+
elsif object_name.present?
|
35
|
+
translation
|
36
|
+
else
|
37
|
+
default_value
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def translation(key = "activerecord.attributes.#{object_name}.#{@attribute}")
|
42
|
+
translate(key, default: default_value)
|
43
|
+
end
|
44
|
+
|
45
|
+
def default_value
|
46
|
+
@attribute.to_s.humanize.capitalize
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def default_attributes
|
52
|
+
return {} unless sorting&.supports?(collection, @attribute)
|
53
|
+
|
54
|
+
{ data: { sort: sorting.status(@attribute) } }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
class HeaderRowComponent < ViewComponent::Base # :nodoc:
|
6
|
+
include HasHtmlAttributes
|
7
|
+
|
8
|
+
renders_many :columns, ->(component) { component }
|
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, **options, &block)
|
28
|
+
with_column(@table.header_cell_component.new(@table, attribute, link: @link_attributes, **options), &block)
|
29
|
+
end
|
30
|
+
|
31
|
+
def header?
|
32
|
+
true
|
33
|
+
end
|
34
|
+
|
35
|
+
def body?
|
36
|
+
false
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
class PagyNavComponent < ViewComponent::Base # :nodoc:
|
6
|
+
include Pagy::Frontend
|
7
|
+
|
8
|
+
attr_reader :pagy_options
|
9
|
+
|
10
|
+
def initialize(collection: nil, pagy: nil, **pagy_options)
|
11
|
+
super()
|
12
|
+
|
13
|
+
pagy ||= collection&.pagination if collection.respond_to?(:pagination)
|
14
|
+
|
15
|
+
raise ArgumentError, "pagy is required" if pagy.blank?
|
16
|
+
|
17
|
+
@pagy = pagy
|
18
|
+
@pagy_options = pagy_options
|
19
|
+
end
|
20
|
+
|
21
|
+
def call
|
22
|
+
pagy_nav(@pagy, **pagy_options).html_safe # rubocop:disable Rails/OutputSafety
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Turbo
|
5
|
+
class PagyNavComponent < Tables::PagyNavComponent # :nodoc:
|
6
|
+
include Tables::TurboReplaceable
|
7
|
+
|
8
|
+
def initialize(id:, **options)
|
9
|
+
super(pagy_id: id, **options)
|
10
|
+
end
|
11
|
+
|
12
|
+
def id
|
13
|
+
pagy_options[:pagy_id]
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def pagy_options
|
19
|
+
super.merge(link_extra: "data-turbo-stream")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Turbo
|
5
|
+
# Renders a table that uses turbo stream replacement when sorting or
|
6
|
+
# paginating.
|
7
|
+
class TableComponent < ::Katalyst::TableComponent
|
8
|
+
include Tables::TurboReplaceable
|
9
|
+
|
10
|
+
def initialize(collection:, id:, header: true, **options)
|
11
|
+
header = if header.is_a?(Hash)
|
12
|
+
default_header_options.merge(header)
|
13
|
+
elsif header
|
14
|
+
default_header_options
|
15
|
+
end
|
16
|
+
|
17
|
+
super(collection: collection, header: header, id: id, **options)
|
18
|
+
end
|
19
|
+
|
20
|
+
def id
|
21
|
+
html_attributes[:id]
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def default_attributes
|
27
|
+
{
|
28
|
+
data: {
|
29
|
+
controller: "tables--turbo-collection",
|
30
|
+
tables__turbo_collection_url_value: current_path,
|
31
|
+
tables__turbo_collection_sort_value: collection.sort
|
32
|
+
}
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def current_path
|
37
|
+
params = collection.to_params
|
38
|
+
query_string = params.empty? ? "" : "?#{Rack::Utils.build_nested_query(params)}"
|
39
|
+
|
40
|
+
"#{request.path}#{query_string}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def default_header_options
|
44
|
+
{ link: { data: { turbo_stream: "" } } }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Collection
|
6
|
+
module Core # :nodoc:
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
include ActiveModel::Model
|
10
|
+
include ActiveModel::Attributes
|
11
|
+
include ActiveModel::Dirty
|
12
|
+
include ActiveSupport::Configurable
|
13
|
+
|
14
|
+
included do
|
15
|
+
class_attribute :reducers, default: ActionDispatch::MiddlewareStack.new
|
16
|
+
|
17
|
+
class << self
|
18
|
+
delegate :use, :before, to: :reducers
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_accessor :items
|
22
|
+
|
23
|
+
delegate :model, :to_model, :each, :count, :empty?, to: :items, allow_nil: true
|
24
|
+
delegate :model_name, to: :model, allow_nil: true
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(**options)
|
28
|
+
super
|
29
|
+
|
30
|
+
clear_changes_information
|
31
|
+
end
|
32
|
+
|
33
|
+
def filter
|
34
|
+
# no-op by default
|
35
|
+
end
|
36
|
+
|
37
|
+
def filtered?
|
38
|
+
!self.class.new.filters.eql?(filters)
|
39
|
+
end
|
40
|
+
|
41
|
+
def filters
|
42
|
+
attributes.except("page", "sort")
|
43
|
+
end
|
44
|
+
|
45
|
+
def apply(items)
|
46
|
+
@items = items
|
47
|
+
reducers.build do |_|
|
48
|
+
filter
|
49
|
+
self
|
50
|
+
end.call(self)
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
def with_params(params)
|
55
|
+
self.attributes = params.permit(self.class.attribute_types.keys)
|
56
|
+
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns a hash of the current attributes that have changed from defaults.
|
61
|
+
def to_params
|
62
|
+
attributes.slice(*changed)
|
63
|
+
end
|
64
|
+
|
65
|
+
def inspect
|
66
|
+
"#<#{self.class.name} @attributes=#{attributes.inspect} @model=\"#{model}\" @count=#{items&.count}>"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pagy/backend"
|
4
|
+
|
5
|
+
module Katalyst
|
6
|
+
module Tables
|
7
|
+
module Collection
|
8
|
+
# Adds pagination support for a collection.
|
9
|
+
#
|
10
|
+
# Pagination will be applied if the collection is configured to paginate
|
11
|
+
# by either specifying `config.paginate = true` or passing
|
12
|
+
# `paginate: true` to the initializer.
|
13
|
+
#
|
14
|
+
# If the value given to `paginate` is a hash, it will be passed to the
|
15
|
+
# `pagy` gem as options.
|
16
|
+
#
|
17
|
+
# If `page` is present in params it will be passed to pagy.
|
18
|
+
module Pagination
|
19
|
+
extend ActiveSupport::Concern
|
20
|
+
|
21
|
+
included do
|
22
|
+
attr_accessor :pagination
|
23
|
+
|
24
|
+
attribute :page, :integer, default: 1
|
25
|
+
|
26
|
+
config_accessor :paginate
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(paginate: config.paginate, **options)
|
30
|
+
super(**options)
|
31
|
+
|
32
|
+
@paginate = paginate
|
33
|
+
end
|
34
|
+
|
35
|
+
def paginate?
|
36
|
+
!!@paginate
|
37
|
+
end
|
38
|
+
|
39
|
+
def paginate_options
|
40
|
+
@paginate.is_a?(Hash) ? @paginate : {}
|
41
|
+
end
|
42
|
+
|
43
|
+
class Paginate # :nodoc:
|
44
|
+
include Pagy::Backend
|
45
|
+
|
46
|
+
def initialize(app)
|
47
|
+
@app = app
|
48
|
+
end
|
49
|
+
|
50
|
+
def call(collection)
|
51
|
+
@collection = @app.call(collection)
|
52
|
+
if collection.paginate?
|
53
|
+
@collection.pagination, @collection.items = pagy(@collection.items, collection.paginate_options)
|
54
|
+
end
|
55
|
+
@collection
|
56
|
+
end
|
57
|
+
|
58
|
+
# pagy shim
|
59
|
+
def params
|
60
|
+
@collection.attributes.with_indifferent_access
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pagy/backend"
|
4
|
+
|
5
|
+
module Katalyst
|
6
|
+
module Tables
|
7
|
+
module Collection
|
8
|
+
# Adds sorting support to a collection.
|
9
|
+
#
|
10
|
+
# Sorting will be applied if the collection is configured with a default
|
11
|
+
# sorting configuration by either specifying
|
12
|
+
# `config.sorting = "column direction"` or passing
|
13
|
+
# `sorting: "column direction"` to the initializer.
|
14
|
+
#
|
15
|
+
# If `sort` is present in params it will override the default sorting.
|
16
|
+
module Sorting
|
17
|
+
extend ActiveSupport::Concern
|
18
|
+
|
19
|
+
included do
|
20
|
+
config_accessor :sorting
|
21
|
+
attr_accessor :sorting
|
22
|
+
|
23
|
+
attribute :sort, :string
|
24
|
+
end
|
25
|
+
|
26
|
+
def initialize(sorting: config.sorting, **options)
|
27
|
+
@sorting = Backend::SortForm.parse(sorting) if sorting
|
28
|
+
|
29
|
+
super(sort: sorting, **options) # set default sort based on config
|
30
|
+
end
|
31
|
+
|
32
|
+
def sort=(value)
|
33
|
+
return unless @sorting
|
34
|
+
|
35
|
+
# update internal proxy
|
36
|
+
@sorting = Backend::SortForm.parse(value, default: attribute_was(:sort))
|
37
|
+
|
38
|
+
# update attribute based on normalized value
|
39
|
+
super(@sorting.to_param)
|
40
|
+
end
|
41
|
+
|
42
|
+
class Sort # :nodoc:
|
43
|
+
include Backend
|
44
|
+
|
45
|
+
def initialize(app)
|
46
|
+
@app = app
|
47
|
+
end
|
48
|
+
|
49
|
+
def call(collection)
|
50
|
+
@collection = @app.call(collection)
|
51
|
+
@collection.sorting, @collection.items = @collection.sorting.apply(@collection.items) if @collection.sorting
|
52
|
+
@collection
|
53
|
+
end
|
54
|
+
|
55
|
+
# pagy shim
|
56
|
+
def params
|
57
|
+
@collection.attributes
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|