katalyst-tables 3.2.0 → 3.3.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +25 -0
- data/app/assets/stylesheets/katalyst/tables/_filter.scss +9 -0
- data/app/components/katalyst/tables/filter/modal_component.html.erb +6 -6
- data/app/components/katalyst/tables/filter/modal_component.rb +57 -11
- data/app/components/katalyst/tables/filter_component.html.erb +1 -3
- data/app/models/concerns/katalyst/tables/collection/core.rb +19 -3
- data/app/models/concerns/katalyst/tables/collection/filtering.rb +2 -65
- data/app/models/concerns/katalyst/tables/collection/pagination.rb +1 -1
- data/app/models/concerns/katalyst/tables/collection/query/parser.rb +9 -1
- data/app/models/concerns/katalyst/tables/collection/query.rb +12 -20
- data/app/models/concerns/katalyst/tables/collection/sorting.rb +1 -1
- data/app/models/katalyst/tables/collection/type/boolean.rb +22 -0
- data/app/models/katalyst/tables/collection/type/date.rb +60 -0
- data/app/models/katalyst/tables/collection/type/enum.rb +21 -0
- data/app/models/katalyst/tables/collection/type/float.rb +57 -0
- data/app/models/katalyst/tables/collection/type/helpers/delegate.rb +50 -0
- data/app/models/katalyst/tables/collection/type/helpers/extensions.rb +28 -0
- data/app/models/katalyst/tables/collection/type/helpers/multiple.rb +30 -0
- data/app/models/katalyst/tables/collection/type/integer.rb +57 -0
- data/app/models/katalyst/tables/collection/type/query.rb +19 -0
- data/app/models/katalyst/tables/collection/type/search.rb +26 -0
- data/app/models/katalyst/tables/collection/type/string.rb +35 -0
- data/app/models/katalyst/tables/collection/type/value.rb +66 -0
- data/app/models/katalyst/tables/collection/type.rb +39 -0
- metadata +15 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1431a1666522842053ab70ae3da4f2610bf6fd819be4fe289d992f17c78e0d87
|
4
|
+
data.tar.gz: be2d0612b1620a052c58e6115b46e388ce3b54bd732d86ba45681bc679e19d68
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9d04228e59216901da2268c3aac3dc6076f623fca6c61121a896b215d278265ae2a2fc5f06df97652be02a53a409eefe08fa7d21892a8d28c5d5457efe90a138
|
7
|
+
data.tar.gz: 25adbd55a147762f4a28aea24f194032eef8e1c27167bfca66a40bd48055b7a018235c94bbe95d6b1a7fe4d49d62d9f5c402034936a0755b394f923cab3668dc
|
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,24 @@
|
|
1
|
+
## [3.3.0]
|
2
|
+
- Custom types for collections which support extensions for filtering with ranges and arrays.
|
3
|
+
|
4
|
+
Note that if you have custom types defined for use in your collections, this could be a breaking
|
5
|
+
change. We recommend you change your types to inherit from `Katalyst::Tables::Collections::Type::Value`
|
6
|
+
and register them with `Katalyst::Tables::Collections::Type`.
|
7
|
+
|
8
|
+
`Query` and `Filtering` are still optional extensions. We recommend that you add them to a
|
9
|
+
based collection class in your application, e.g. `Admin::Collection`.
|
10
|
+
|
1
11
|
## [3.2.0]
|
2
12
|
- Enum columns
|
3
13
|
- Filter component (still in development, optional extension)
|
4
14
|
|
15
|
+
Note: this release adds a dedicated registry for collection filtering types
|
16
|
+
instead of using the default provided by ActiveModel.
|
17
|
+
|
18
|
+
If you have custom types that you use with tables collections,
|
19
|
+
you will need to register them with Katalyst::Tables::Collection::Type
|
20
|
+
in an initializer or similar.
|
21
|
+
|
5
22
|
## [3.1.0]
|
6
23
|
- Introduce summary tables
|
7
24
|
- Update ruby requirement >= 3.3
|
data/README.md
CHANGED
@@ -37,6 +37,8 @@ This gem provides entry points for backend and frontend concerns:
|
|
37
37
|
* `Katalyst::Tables::Frontend` provides `table_with` for inline table generation,
|
38
38
|
* `Katalyst::Tables::Collection::Base` provides a default entry point for
|
39
39
|
building collections in your controller actions
|
40
|
+
* `Katalyst::Tables::Collection::Query` provides build-in query parsing and filtering
|
41
|
+
based on the attributes defined in your collection.
|
40
42
|
|
41
43
|
### Frontend
|
42
44
|
|
@@ -206,6 +208,29 @@ detect features such as sorting and generate the appropriate table header links.
|
|
206
208
|
<%= table_with(collection:) %>
|
207
209
|
```
|
208
210
|
|
211
|
+
### Query
|
212
|
+
|
213
|
+
Include `Katalyst::Tables::Collection::Query` into your collection to add automatic
|
214
|
+
query parsing and filtering based on the configured attributes. For example:
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
class Collection < Katalyst::Tables::Collection::Base
|
218
|
+
include Katalyst::Tables::Collection::Query
|
219
|
+
|
220
|
+
attribute :first_name, :string
|
221
|
+
attribute :created_at, :date
|
222
|
+
end
|
223
|
+
```
|
224
|
+
|
225
|
+
With this definition and a text-input named `query`, your users can write tagged
|
226
|
+
query expressions such as `first_name:Aaron` or `created_at:>2024-01-01` and these
|
227
|
+
will be automatically parsed and applied to the collection attribute, and the collection
|
228
|
+
will automatically generate and apply ActiveRecord conditions to filter the given scope.
|
229
|
+
|
230
|
+
There's also a frontend utility, `filter_with(collection:)` that will generate the form
|
231
|
+
and show a modal that helps users to interact with the query interface. More details available
|
232
|
+
in the [query](docs/query.md) documentation.
|
233
|
+
|
209
234
|
## Summary tables
|
210
235
|
You can use the `Katalyst::SummaryTableComponent` to render a single record utilizing all the functionality from the
|
211
236
|
`Katalyst::TableComponent`.
|
@@ -2,17 +2,17 @@
|
|
2
2
|
<table>
|
3
3
|
<thead>
|
4
4
|
<tr>
|
5
|
-
<th></th>
|
6
|
-
<th>Key</th>
|
7
|
-
<th>Values</th>
|
5
|
+
<th class="label"></th>
|
6
|
+
<th class="key">Key</th>
|
7
|
+
<th class="values">Values</th>
|
8
8
|
</tr>
|
9
9
|
</thead>
|
10
10
|
<tbody>
|
11
11
|
<% attributes.each do |key, attribute| %>
|
12
12
|
<tr>
|
13
|
-
<th><%= collection.model.human_attribute_name(key) %></th>
|
14
|
-
<td><%= key %></td>
|
15
|
-
<td><%= values_for(key, attribute) %></td>
|
13
|
+
<th class="label"><%= collection.model.human_attribute_name(key) %></th>
|
14
|
+
<td class="key"><%= key %></td>
|
15
|
+
<td class="values"><%= values_for(key, attribute) %></td>
|
16
16
|
</tr>
|
17
17
|
<% end %>
|
18
18
|
</tbody>
|
@@ -30,27 +30,73 @@ module Katalyst
|
|
30
30
|
}
|
31
31
|
end
|
32
32
|
|
33
|
+
using Collection::Type::Helpers::Extensions
|
34
|
+
|
33
35
|
def attributes
|
34
|
-
collection.class.attribute_types
|
36
|
+
collection.class.attribute_types
|
37
|
+
.select { |_, a| a.filterable? && a.type != :search }
|
35
38
|
end
|
36
39
|
|
37
40
|
def values_for(key, attribute)
|
38
41
|
values_method = "#{key.parameterize.underscore}_values"
|
39
|
-
if
|
42
|
+
if collection.respond_to?(values_method)
|
43
|
+
return scope_values(attribute, values_method)
|
44
|
+
end
|
45
|
+
|
46
|
+
case attribute.type
|
47
|
+
when :boolean
|
40
48
|
render_options(true, false)
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
+
when :date
|
50
|
+
date_values
|
51
|
+
when :integer
|
52
|
+
integer_values(attribute)
|
53
|
+
when :float
|
54
|
+
float_values(attribute)
|
55
|
+
when :enum
|
56
|
+
enum_values(key)
|
57
|
+
when :string
|
58
|
+
string_values(attribute)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def scope_values(attribute, values_method)
|
63
|
+
values = collection.public_send(values_method)
|
64
|
+
attribute.multiple? ? render_array(*values) : render_options(*values)
|
65
|
+
end
|
66
|
+
|
67
|
+
def date_values
|
68
|
+
render_options("YYYY-MM-DD", ">YYYY-MM-DD", "<YYYY-MM-DD", "YYYY-MM-DD..YYYY-MM-DD")
|
69
|
+
end
|
70
|
+
|
71
|
+
def string_values(attribute)
|
72
|
+
options = render_options("example", '"an example"')
|
73
|
+
safe_join([options, attribute.exact? ? "(exact match)" : "(fuzzy match)"], " ")
|
74
|
+
end
|
75
|
+
|
76
|
+
def enum_values(key)
|
77
|
+
enums = collection.model.defined_enums
|
78
|
+
|
79
|
+
render_array(*enums[key].keys) if enums.has_key?(key)
|
80
|
+
end
|
81
|
+
|
82
|
+
def float_values(attribute)
|
83
|
+
if attribute.multiple?
|
84
|
+
render_array("0.5", "1", "...")
|
85
|
+
else
|
86
|
+
render_options("0.5", ">0.5", "<0.5", "-0.5..0.5")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def integer_values(attribute)
|
91
|
+
if attribute.multiple?
|
92
|
+
render_array("0", "1", "...")
|
93
|
+
else
|
94
|
+
render_options("10", ">10", "<10", "0..10")
|
49
95
|
end
|
50
96
|
end
|
51
97
|
|
52
98
|
def render_option(value)
|
53
|
-
|
99
|
+
tag.code(value.to_s)
|
54
100
|
end
|
55
101
|
|
56
102
|
def render_options(*values)
|
@@ -6,13 +6,11 @@
|
|
6
6
|
<%= form.text_field(
|
7
7
|
:query,
|
8
8
|
type: :search,
|
9
|
-
size: :full,
|
10
|
-
label: nil,
|
11
9
|
autocomplete: "off",
|
12
10
|
**input_attributes,
|
13
11
|
) %>
|
14
12
|
|
15
|
-
<%= form.
|
13
|
+
<%= form.button("Apply", type: :submit, name: nil) %>
|
16
14
|
<% end %>
|
17
15
|
<% end %>
|
18
16
|
|
@@ -26,8 +26,22 @@ module Katalyst
|
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
-
|
30
|
-
|
29
|
+
using Type::Helpers::Extensions
|
30
|
+
|
31
|
+
def attribute(name, type = nil, default: (no_default = true), **)
|
32
|
+
type = type.is_a?(Symbol) ? resolve_type_name(type, **) : type || Type::Value.new
|
33
|
+
|
34
|
+
default = type.default_value if no_default
|
35
|
+
|
36
|
+
default.nil? && no_default ? super(name, type, **) : super
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# @override ActiveModel::AttributeRegistration::ClassMethods#resolve_type_name()
|
42
|
+
def resolve_type_name(name, **)
|
43
|
+
# note, this is Katalyst::Tables::Collection::Type, not ActiveModel::Type
|
44
|
+
Type.lookup(name, **)
|
31
45
|
end
|
32
46
|
end
|
33
47
|
|
@@ -72,7 +86,9 @@ module Katalyst
|
|
72
86
|
end
|
73
87
|
|
74
88
|
def filters
|
75
|
-
changes
|
89
|
+
changes
|
90
|
+
.select { |k, _| self.class._default_attributes[k].type.filterable? }
|
91
|
+
.transform_values(&:second)
|
76
92
|
end
|
77
93
|
|
78
94
|
def model
|
@@ -6,8 +6,6 @@ module Katalyst
|
|
6
6
|
module Filtering
|
7
7
|
extend ActiveSupport::Concern
|
8
8
|
|
9
|
-
DEFAULT_ATTRIBUTES = %w[sort page query].freeze
|
10
|
-
|
11
9
|
included do
|
12
10
|
use(Filter)
|
13
11
|
end
|
@@ -20,73 +18,12 @@ module Katalyst
|
|
20
18
|
end
|
21
19
|
|
22
20
|
def call(collection)
|
23
|
-
collection.
|
24
|
-
|
25
|
-
|
26
|
-
next if DEFAULT_ATTRIBUTES.include?(key)
|
27
|
-
|
28
|
-
value = collection.attributes[key]
|
29
|
-
|
30
|
-
filter_attribute(collection, key, value, attribute.type.type)
|
21
|
+
collection.instance_variable_get(:@attributes).each_value do |attribute|
|
22
|
+
collection.items = attribute.type.filter(collection.items, attribute)
|
31
23
|
end
|
32
24
|
|
33
25
|
@app.call(collection)
|
34
26
|
end
|
35
|
-
|
36
|
-
def filter_attribute(collection, key, value, type)
|
37
|
-
if key == "search"
|
38
|
-
search(collection, value)
|
39
|
-
elsif type == :string
|
40
|
-
filter_matches(collection, key, value)
|
41
|
-
elsif type == :boolean
|
42
|
-
filter_eq(collection, key, value) unless value.nil?
|
43
|
-
elsif value.present?
|
44
|
-
filter_eq(collection, key, value)
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def search(collection, search)
|
49
|
-
return if search.blank? || !collection.searchable?
|
50
|
-
|
51
|
-
collection.items = collection.items.public_send(collection.config.search_scope, search)
|
52
|
-
end
|
53
|
-
|
54
|
-
def filter_matches(collection, key, value)
|
55
|
-
return if value.nil?
|
56
|
-
|
57
|
-
model, column = join_key(collection, key)
|
58
|
-
arel_column = model.arel_table[column]
|
59
|
-
|
60
|
-
collection.items = collection.items.where(arel_column.matches("%#{sanitize_sql_like(value)}%"))
|
61
|
-
end
|
62
|
-
|
63
|
-
def filter_eq(collection, key, value)
|
64
|
-
model, column = join_key(collection, key)
|
65
|
-
|
66
|
-
condition = if model.attribute_types.has_key?(column)
|
67
|
-
model.where(column => value)
|
68
|
-
else
|
69
|
-
model.public_send(column, value)
|
70
|
-
end
|
71
|
-
|
72
|
-
collection.items = collection.items.merge(condition)
|
73
|
-
end
|
74
|
-
|
75
|
-
private
|
76
|
-
|
77
|
-
def join_key(collection, key)
|
78
|
-
if key.include?(".")
|
79
|
-
table, column = key.split(".")
|
80
|
-
collection.items = collection.items.joins(table.to_sym)
|
81
|
-
[collection.items.reflections[table].klass, column]
|
82
|
-
else
|
83
|
-
[collection.items.model, key]
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
def column_for(key)
|
88
|
-
key.include?(".") ? key.split(".").last : key
|
89
|
-
end
|
90
27
|
end
|
91
28
|
end
|
92
29
|
end
|
@@ -25,6 +25,10 @@ module Katalyst
|
|
25
25
|
break unless take_tagged || take_untagged
|
26
26
|
end
|
27
27
|
|
28
|
+
if untagged.any? && (search = collection.class.search_attribute)
|
29
|
+
collection.assign_attributes(search => untagged.join(" "))
|
30
|
+
end
|
31
|
+
|
28
32
|
self
|
29
33
|
end
|
30
34
|
|
@@ -38,6 +42,8 @@ module Katalyst
|
|
38
42
|
return unless query.scan(/(\w+(\.\w+)?):/)
|
39
43
|
|
40
44
|
key, = query.values_at(1)
|
45
|
+
skip_whitespace
|
46
|
+
|
41
47
|
parser_for(key).parse(query)
|
42
48
|
end
|
43
49
|
|
@@ -49,10 +55,12 @@ module Katalyst
|
|
49
55
|
untagged
|
50
56
|
end
|
51
57
|
|
58
|
+
using Type::Helpers::Extensions
|
59
|
+
|
52
60
|
def parser_for(key)
|
53
61
|
attribute = collection.class._default_attributes[key]
|
54
62
|
|
55
|
-
if
|
63
|
+
if attribute.type.multiple? || attribute.value.is_a?(::Array)
|
56
64
|
ArrayValueParser.new(collection:, attribute:)
|
57
65
|
else
|
58
66
|
SingleValueParser.new(collection:, attribute:)
|
@@ -8,35 +8,27 @@ module Katalyst
|
|
8
8
|
|
9
9
|
include Filtering
|
10
10
|
|
11
|
-
|
12
|
-
|
11
|
+
class_methods do
|
12
|
+
def search_attribute
|
13
|
+
_default_attributes.each_value do |attribute|
|
14
|
+
return attribute.name if attribute.type.type == :search
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
13
18
|
|
14
|
-
|
15
|
-
attribute :
|
19
|
+
included do
|
20
|
+
attribute :q, :query, default: ""
|
21
|
+
alias_attribute :query, :q
|
16
22
|
|
17
23
|
# Note: this is defined inline so that we can overwrite query=
|
18
|
-
def
|
24
|
+
def q=(value)
|
19
25
|
query = super
|
20
26
|
|
21
|
-
|
22
|
-
|
23
|
-
if searchable? && parser.untagged.any?
|
24
|
-
self.search = parser.untagged.join(" ")
|
25
|
-
end
|
27
|
+
Parser.new(self).parse(query)
|
26
28
|
|
27
29
|
query
|
28
30
|
end
|
29
31
|
end
|
30
|
-
|
31
|
-
# Returns true if the collection supports untagged searching. This
|
32
|
-
# requires config.search_scope to be set to the name of the scope to use
|
33
|
-
# in the target record for untagged text searches. If not set, untagged
|
34
|
-
# search terms will be silently ignored.
|
35
|
-
#
|
36
|
-
# @return [true, false]
|
37
|
-
def searchable?
|
38
|
-
config.search_scope.present?
|
39
|
-
end
|
40
32
|
end
|
41
33
|
end
|
42
34
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Collection
|
6
|
+
module Type
|
7
|
+
class Boolean < Value
|
8
|
+
include Helpers::Delegate
|
9
|
+
include Helpers::Multiple
|
10
|
+
|
11
|
+
def initialize(**)
|
12
|
+
super(**, delegate: ActiveModel::Type::Boolean)
|
13
|
+
end
|
14
|
+
|
15
|
+
def filter?(attribute, value)
|
16
|
+
(!value.nil? && !value.eql?([])) || attribute.came_from_user?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Collection
|
6
|
+
module Type
|
7
|
+
class Date < Value
|
8
|
+
def type
|
9
|
+
:date
|
10
|
+
end
|
11
|
+
|
12
|
+
def serialize(value)
|
13
|
+
if value.is_a?(Date)
|
14
|
+
value.to_fs(:db)
|
15
|
+
elsif value.is_a?(Range)
|
16
|
+
if value.begin.nil?
|
17
|
+
"<#{value.end.to_fs(:db)}"
|
18
|
+
elsif value.end.nil?
|
19
|
+
">#{value.begin.to_fs(:db)}"
|
20
|
+
else
|
21
|
+
"#{value.begin.to_fs(:db)}..#{value.end.to_fs(:db)}"
|
22
|
+
end
|
23
|
+
else
|
24
|
+
value.to_s
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
|
31
|
+
LOWER_BOUND = /\A>(\d{4})-(\d\d)-(\d\d)\z/
|
32
|
+
UPPER_BOUND = /\A<(\d{4})-(\d\d)-(\d\d)\z/
|
33
|
+
BOUNDED = /\A(\d{4})-(\d\d)-(\d\d)\.\.(\d{4})-(\d\d)-(\d\d)\z/
|
34
|
+
|
35
|
+
def cast_value(value)
|
36
|
+
return value unless value.is_a?(::String)
|
37
|
+
|
38
|
+
if value =~ ISO_DATE
|
39
|
+
new_date($1.to_i, $2.to_i, $3.to_i)
|
40
|
+
elsif value =~ LOWER_BOUND
|
41
|
+
(new_date($1.to_i, $2.to_i, $3.to_i)..)
|
42
|
+
elsif value =~ UPPER_BOUND
|
43
|
+
(..new_date($1.to_i, $2.to_i, $3.to_i))
|
44
|
+
elsif value =~ BOUNDED
|
45
|
+
(new_date($1.to_i, $2.to_i, $3.to_i)..new_date($4.to_i, $5.to_i, $6.to_i))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def new_date(year, mon, mday)
|
50
|
+
return nil if year.nil? || (year.zero? && mon.zero? && mday.zero?)
|
51
|
+
|
52
|
+
::Date.new(year, mon, mday)
|
53
|
+
rescue ArgumentError, TypeError
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Collection
|
6
|
+
module Type
|
7
|
+
class Enum < Value
|
8
|
+
include Helpers::Multiple
|
9
|
+
|
10
|
+
def initialize(multiple: true, **)
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
def type
|
15
|
+
:enum
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Collection
|
6
|
+
module Type
|
7
|
+
class Float < Value
|
8
|
+
include Helpers::Delegate
|
9
|
+
include Helpers::Multiple
|
10
|
+
|
11
|
+
def initialize(**)
|
12
|
+
super(**, delegate: ActiveModel::Type::Float)
|
13
|
+
end
|
14
|
+
|
15
|
+
def serialize(value)
|
16
|
+
if value.is_a?(Range)
|
17
|
+
if value.begin.nil?
|
18
|
+
"<#{super(value.end)}"
|
19
|
+
elsif value.end.nil?
|
20
|
+
">#{super(value.begin)}"
|
21
|
+
else
|
22
|
+
"#{super(value.begin)}..#{super(value.end)}"
|
23
|
+
end
|
24
|
+
else
|
25
|
+
super
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
FLOAT = /(-?\d+(?:\.\d+)?)/
|
32
|
+
SINGLE_VALUE = /\A#{FLOAT}\z/
|
33
|
+
LOWER_BOUND = /\A>#{FLOAT}\z/
|
34
|
+
UPPER_BOUND = /\A<#{FLOAT}\z/
|
35
|
+
BOUNDED = /\A#{FLOAT}\.\.#{FLOAT}\z/
|
36
|
+
|
37
|
+
def cast_value(value)
|
38
|
+
case value
|
39
|
+
when ::Range, ::Integer
|
40
|
+
value
|
41
|
+
when SINGLE_VALUE
|
42
|
+
super($1)
|
43
|
+
when LOWER_BOUND
|
44
|
+
((super($1))..)
|
45
|
+
when UPPER_BOUND
|
46
|
+
(..(super($1)))
|
47
|
+
when BOUNDED
|
48
|
+
((super($1))..(super($2)))
|
49
|
+
else
|
50
|
+
super
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Collection
|
6
|
+
module Type
|
7
|
+
module Helpers
|
8
|
+
# Lifts a delegating type from value to arrays of values
|
9
|
+
module Delegate
|
10
|
+
delegate :type, to: :@delegate
|
11
|
+
|
12
|
+
def initialize(delegate:, **arguments)
|
13
|
+
super(**arguments)
|
14
|
+
|
15
|
+
@delegate = delegate.new(**arguments.except(:filter, :multiple, :scope))
|
16
|
+
end
|
17
|
+
|
18
|
+
using Extensions
|
19
|
+
|
20
|
+
def deserialize(value)
|
21
|
+
if multiple? && value.is_a?(::Array)
|
22
|
+
value.map { |v| @delegate.deserialize(v) }
|
23
|
+
else
|
24
|
+
@delegate.deserialize(value)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def serialize(value)
|
29
|
+
if multiple? && value.is_a?(::Array)
|
30
|
+
value.map { |v| @delegate.serialize(v) }
|
31
|
+
else
|
32
|
+
@delegate.serialize(value)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def cast_value(value)
|
39
|
+
if multiple? && value.is_a?(::Array)
|
40
|
+
value.map { |v| @delegate.cast(v) }
|
41
|
+
else
|
42
|
+
@delegate.cast(value)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Collection
|
6
|
+
module Type
|
7
|
+
module Helpers
|
8
|
+
# Adds support default_value, multiple?, and filterable? to ActiveModel::Type::Value
|
9
|
+
module Extensions
|
10
|
+
refine(::ActiveModel::Type::Value) do
|
11
|
+
def default_value
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def multiple?
|
16
|
+
false
|
17
|
+
end
|
18
|
+
|
19
|
+
def filterable?
|
20
|
+
false
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Collection
|
6
|
+
module Type
|
7
|
+
module Helpers
|
8
|
+
# Adds support for multiple: true
|
9
|
+
module Multiple
|
10
|
+
def initialize(multiple: false, **)
|
11
|
+
super(**)
|
12
|
+
|
13
|
+
@multiple = multiple
|
14
|
+
end
|
15
|
+
|
16
|
+
def multiple?
|
17
|
+
@multiple
|
18
|
+
end
|
19
|
+
|
20
|
+
using Extensions
|
21
|
+
|
22
|
+
def default_value
|
23
|
+
multiple? ? [] : super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Collection
|
6
|
+
module Type
|
7
|
+
class Integer < Value
|
8
|
+
include Helpers::Delegate
|
9
|
+
include Helpers::Multiple
|
10
|
+
|
11
|
+
def initialize(**)
|
12
|
+
super(**, delegate: ActiveModel::Type::Integer)
|
13
|
+
end
|
14
|
+
|
15
|
+
def serialize(value)
|
16
|
+
if value.is_a?(Range)
|
17
|
+
if value.begin.nil?
|
18
|
+
"<#{super(value.end)}"
|
19
|
+
elsif value.end.nil?
|
20
|
+
">#{super(value.begin)}"
|
21
|
+
else
|
22
|
+
"#{super(value.begin)}..#{super(value.end)}"
|
23
|
+
end
|
24
|
+
else
|
25
|
+
super
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
INTEGER = /(-?\d+)/
|
32
|
+
SINGLE_VALUE = /\A#{INTEGER}\z/
|
33
|
+
LOWER_BOUND = /\A>#{INTEGER}\z/
|
34
|
+
UPPER_BOUND = /\A<#{INTEGER}\z/
|
35
|
+
BOUNDED = /\A#{INTEGER}\.\.#{INTEGER}\z/
|
36
|
+
|
37
|
+
def cast_value(value)
|
38
|
+
case value
|
39
|
+
when ::Range, ::Integer
|
40
|
+
value
|
41
|
+
when SINGLE_VALUE
|
42
|
+
super($1)
|
43
|
+
when LOWER_BOUND
|
44
|
+
((super($1))..)
|
45
|
+
when UPPER_BOUND
|
46
|
+
(..(super($1)))
|
47
|
+
when BOUNDED
|
48
|
+
((super($1))..(super($2)))
|
49
|
+
else
|
50
|
+
super
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Collection
|
6
|
+
module Type
|
7
|
+
class Search < Value
|
8
|
+
# @overwrite Value.initialize() to require scope
|
9
|
+
# rubocop:disable Lint/UselessMethodDefinition
|
10
|
+
def initialize(scope:, **)
|
11
|
+
super
|
12
|
+
end
|
13
|
+
# rubocop:enable Lint/UselessMethodDefinition
|
14
|
+
|
15
|
+
def type
|
16
|
+
:search
|
17
|
+
end
|
18
|
+
|
19
|
+
def filter_condition(model, _, value)
|
20
|
+
model.public_send(scope, value)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Collection
|
6
|
+
module Type
|
7
|
+
class String < Value
|
8
|
+
include ActiveRecord::Sanitization::ClassMethods
|
9
|
+
|
10
|
+
attr_reader :exact
|
11
|
+
alias_method :exact?, :exact
|
12
|
+
|
13
|
+
delegate :type, :serialize, :deserialize, :cast, to: :@delegate
|
14
|
+
|
15
|
+
def initialize(exact: false, **)
|
16
|
+
super(**)
|
17
|
+
|
18
|
+
@exact = exact
|
19
|
+
@delegate = ActiveModel::Type::String.new
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def filter_condition(model, column, value)
|
25
|
+
if exact? || scope
|
26
|
+
super
|
27
|
+
else
|
28
|
+
model.where(model.arel_table[column].matches("%#{sanitize_sql_like(value)}%"))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Collection
|
6
|
+
module Type
|
7
|
+
class Value < ActiveModel::Type::Value
|
8
|
+
using Helpers::Extensions
|
9
|
+
|
10
|
+
attr_reader :scope
|
11
|
+
|
12
|
+
def initialize(scope: nil, filter: true)
|
13
|
+
super()
|
14
|
+
|
15
|
+
@scope = scope
|
16
|
+
@filterable = filter
|
17
|
+
end
|
18
|
+
|
19
|
+
def filterable?
|
20
|
+
@filterable
|
21
|
+
end
|
22
|
+
|
23
|
+
def filter?(attribute, value)
|
24
|
+
filterable? && (value.present? || attribute.came_from_user?)
|
25
|
+
end
|
26
|
+
|
27
|
+
def filter(scope, attribute)
|
28
|
+
value = filter_value(attribute)
|
29
|
+
|
30
|
+
return scope unless filter?(attribute, value)
|
31
|
+
|
32
|
+
scope, model, column = model_and_column_for(scope, attribute)
|
33
|
+
condition = filter_condition(model, column, value)
|
34
|
+
|
35
|
+
scope.merge(condition)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def filter_value(attribute)
|
41
|
+
attribute.value
|
42
|
+
end
|
43
|
+
|
44
|
+
def filter_condition(model, column, value)
|
45
|
+
if value.nil?
|
46
|
+
model.none
|
47
|
+
elsif scope
|
48
|
+
model.public_send(scope, value)
|
49
|
+
else
|
50
|
+
model.where(column => value)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def model_and_column_for(scope, attribute)
|
55
|
+
if attribute.name.include?(".")
|
56
|
+
table, column = attribute.name.split(".")
|
57
|
+
[scope.joins(table.to_sym), scope.model.reflections[table].klass, column]
|
58
|
+
else
|
59
|
+
[scope, scope.model, attribute.name]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_model/type"
|
4
|
+
|
5
|
+
module Katalyst
|
6
|
+
module Tables
|
7
|
+
module Collection
|
8
|
+
# Based on ActiveModel::Type – provides a registry for Collection filtering
|
9
|
+
module Type
|
10
|
+
@registry = ActiveModel::Type::Registry.new
|
11
|
+
|
12
|
+
class << self
|
13
|
+
attr_accessor :registry # :nodoc:
|
14
|
+
|
15
|
+
def register(type_name, klass = nil, &)
|
16
|
+
registry.register(type_name, klass, &)
|
17
|
+
end
|
18
|
+
|
19
|
+
def lookup(...)
|
20
|
+
registry.lookup(...)
|
21
|
+
end
|
22
|
+
|
23
|
+
def default_value
|
24
|
+
@default_value ||= Value.new
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
register(:boolean, Type::Boolean)
|
29
|
+
register(:date, Type::Date)
|
30
|
+
register(:enum, Type::Enum)
|
31
|
+
register(:float, Type::Float)
|
32
|
+
register(:integer, Type::Integer)
|
33
|
+
register(:string, Type::String)
|
34
|
+
register(:query, Type::Query)
|
35
|
+
register(:search, Type::Search)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: katalyst-tables
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Katalyst Interactive
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-06-
|
11
|
+
date: 2024-06-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: katalyst-html-attributes
|
@@ -132,6 +132,19 @@ files:
|
|
132
132
|
- app/models/katalyst/tables/collection/array.rb
|
133
133
|
- app/models/katalyst/tables/collection/base.rb
|
134
134
|
- app/models/katalyst/tables/collection/filter.rb
|
135
|
+
- app/models/katalyst/tables/collection/type.rb
|
136
|
+
- app/models/katalyst/tables/collection/type/boolean.rb
|
137
|
+
- app/models/katalyst/tables/collection/type/date.rb
|
138
|
+
- app/models/katalyst/tables/collection/type/enum.rb
|
139
|
+
- app/models/katalyst/tables/collection/type/float.rb
|
140
|
+
- app/models/katalyst/tables/collection/type/helpers/delegate.rb
|
141
|
+
- app/models/katalyst/tables/collection/type/helpers/extensions.rb
|
142
|
+
- app/models/katalyst/tables/collection/type/helpers/multiple.rb
|
143
|
+
- app/models/katalyst/tables/collection/type/integer.rb
|
144
|
+
- app/models/katalyst/tables/collection/type/query.rb
|
145
|
+
- app/models/katalyst/tables/collection/type/search.rb
|
146
|
+
- app/models/katalyst/tables/collection/type/string.rb
|
147
|
+
- app/models/katalyst/tables/collection/type/value.rb
|
135
148
|
- config/importmap.rb
|
136
149
|
- config/locales/tables.en.yml
|
137
150
|
- lib/katalyst/tables.rb
|