katalyst-tables 3.0.0 → 3.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +17 -3
- data/app/assets/builds/katalyst/tables.esm.js +16 -0
- data/app/assets/builds/katalyst/tables.js +16 -0
- data/app/assets/builds/katalyst/tables.min.js +1 -1
- data/app/assets/builds/katalyst/tables.min.js.map +1 -1
- data/app/assets/stylesheets/katalyst/tables/_filter.scss +34 -0
- data/app/assets/stylesheets/katalyst/tables/_index.scss +2 -0
- data/app/assets/stylesheets/katalyst/tables/_select.scss +3 -0
- data/app/assets/stylesheets/katalyst/tables/_summary.scss +14 -0
- data/app/assets/stylesheets/katalyst/tables/_table.scss +3 -0
- data/app/assets/stylesheets/katalyst/tables/typed-columns/_boolean.scss +1 -1
- data/app/assets/stylesheets/katalyst/tables/typed-columns/_currency.scss +3 -0
- data/app/assets/stylesheets/katalyst/tables/typed-columns/_date.scss +1 -1
- data/app/assets/stylesheets/katalyst/tables/typed-columns/_datetime.scss +1 -1
- data/app/assets/stylesheets/katalyst/tables/typed-columns/_enum.scss +9 -0
- data/app/assets/stylesheets/katalyst/tables/typed-columns/_index.scss +1 -0
- data/app/assets/stylesheets/katalyst/tables/typed-columns/_number.scss +3 -0
- data/app/components/concerns/katalyst/tables/has_table_content.rb +2 -2
- data/app/components/concerns/katalyst/tables/row_renderer.rb +1 -1
- data/app/components/concerns/katalyst/tables/sortable.rb +1 -1
- data/app/components/katalyst/summary_table_component.html.erb +15 -0
- data/app/components/katalyst/summary_table_component.rb +44 -0
- data/app/components/katalyst/table_component.rb +28 -0
- data/app/components/katalyst/tables/cells/enum_component.rb +27 -0
- data/app/components/katalyst/tables/filter/modal_component.html.erb +25 -0
- data/app/components/katalyst/tables/filter/modal_component.rb +66 -0
- data/app/components/katalyst/tables/filter_component.html.erb +20 -0
- data/app/components/katalyst/tables/filter_component.rb +91 -0
- data/app/components/katalyst/tables/summary/body_component.html.erb +3 -0
- data/app/components/katalyst/tables/summary/body_component.rb +10 -0
- data/app/components/katalyst/tables/summary/header_component.html.erb +3 -0
- data/app/components/katalyst/tables/summary/header_component.rb +10 -0
- data/app/components/katalyst/tables/summary/row_component.html.erb +4 -0
- data/app/components/katalyst/tables/summary/row_component.rb +12 -0
- data/app/controllers/concerns/katalyst/tables/backend.rb +15 -0
- data/app/helpers/katalyst/tables/frontend.rb +27 -0
- data/app/javascript/tables/application.js +5 -0
- data/app/javascript/tables/filter/modal_controller.js +13 -0
- data/app/models/concerns/katalyst/tables/collection/core.rb +30 -0
- data/app/models/concerns/katalyst/tables/collection/filtering.rb +80 -9
- data/app/models/concerns/katalyst/tables/collection/pagination.rb +2 -2
- data/app/models/concerns/katalyst/tables/collection/query/array_value_parser.rb +56 -0
- data/app/models/concerns/katalyst/tables/collection/query/parser.rb +65 -0
- data/app/models/concerns/katalyst/tables/collection/query/single_value_parser.rb +24 -0
- data/app/models/concerns/katalyst/tables/collection/query/value_parser.rb +34 -0
- data/app/models/concerns/katalyst/tables/collection/query.rb +43 -0
- data/app/models/concerns/katalyst/tables/collection/sorting.rb +2 -2
- data/app/models/katalyst/tables/collection/array.rb +0 -1
- data/app/models/katalyst/tables/collection/base.rb +0 -5
- data/app/models/katalyst/tables/collection/filter.rb +2 -3
- data/config/importmap.rb +1 -0
- metadata +27 -4
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Filter
|
6
|
+
class ModalComponent < ViewComponent::Base
|
7
|
+
include Katalyst::HtmlAttributes
|
8
|
+
include Katalyst::Tables::Frontend
|
9
|
+
|
10
|
+
DEFAULT_ATTRIBUTES = %w[page sort search query].freeze
|
11
|
+
|
12
|
+
renders_one :footer
|
13
|
+
|
14
|
+
attr_reader :collection, :url
|
15
|
+
|
16
|
+
def initialize(collection:, **)
|
17
|
+
super(**)
|
18
|
+
|
19
|
+
@collection = collection
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def default_html_attributes
|
25
|
+
{
|
26
|
+
class: "filter-keys-modal",
|
27
|
+
data: {
|
28
|
+
tables__filter__modal_target: "modal",
|
29
|
+
},
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def attributes
|
34
|
+
collection.class.attribute_types.except(*DEFAULT_ATTRIBUTES)
|
35
|
+
end
|
36
|
+
|
37
|
+
def values_for(key, attribute)
|
38
|
+
values_method = "#{key.parameterize.underscore}_values"
|
39
|
+
if attribute.type == :boolean
|
40
|
+
render_options(true, false)
|
41
|
+
elsif collection.model.defined_enums.has_key?(key)
|
42
|
+
render_array(*collection.model.defined_enums[key].keys)
|
43
|
+
elsif collection.respond_to?(values_method)
|
44
|
+
if collection.class.enum_attribute?(key)
|
45
|
+
render_array(*collection.public_send(values_method))
|
46
|
+
else
|
47
|
+
render_options(*collection.public_send(values_method))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def render_option(value)
|
53
|
+
"<code>#{value}</code>".html_safe # rubocop:disable Rails/OutputSafety
|
54
|
+
end
|
55
|
+
|
56
|
+
def render_options(*values)
|
57
|
+
safe_join(values.map { |value| render_option(value) }, ", ")
|
58
|
+
end
|
59
|
+
|
60
|
+
def render_array(*values)
|
61
|
+
safe_join(["[", render_options(*values), "]"])
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<%= tag.div(**html_attributes) do %>
|
2
|
+
<% if content? %>
|
3
|
+
<%= content %>
|
4
|
+
<% else %>
|
5
|
+
<%= form do |form| %>
|
6
|
+
<%= form.text_field(
|
7
|
+
:query,
|
8
|
+
type: :search,
|
9
|
+
size: :full,
|
10
|
+
label: nil,
|
11
|
+
autocomplete: "off",
|
12
|
+
**input_attributes,
|
13
|
+
) %>
|
14
|
+
|
15
|
+
<%= form.submit("Apply") %>
|
16
|
+
<% end %>
|
17
|
+
<% end %>
|
18
|
+
|
19
|
+
<%= modal %>
|
20
|
+
<% end %>
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
# A component for rendering a data driven filter for a collection.
|
6
|
+
# <%= Katalyst::Tables::FilterComponent.new(collection: @people, url: peoples_path) %>
|
7
|
+
#
|
8
|
+
# By default, the component will render a form containing a single text field. Interacting with the
|
9
|
+
# text field will display a dropdown outlining all available keys and values to be filtered on.
|
10
|
+
#
|
11
|
+
# You can override how the form and input displays by passing in content to the component.
|
12
|
+
# The component provides a helper function `form`
|
13
|
+
# to ensure the correct attributes and default form fields are collected.
|
14
|
+
# You can pass additional options to the `form` method to modify it.
|
15
|
+
#
|
16
|
+
# <%= Katalyst::Tables::FilterComponent.new(collection: @people, url: peoples_path) do |filter| %>
|
17
|
+
# <%= filter.form(builder: GOVUKFormBuilder) do |form| %>
|
18
|
+
# <%= form.govuk_text_field :query %>
|
19
|
+
# <%= form.govuk_submit "Apply" %>
|
20
|
+
# <% end %>
|
21
|
+
# <% end %>
|
22
|
+
#
|
23
|
+
#
|
24
|
+
# Additionally the component allows for access to the dropdown that displays when interacting with the input.
|
25
|
+
# The dropdown supports additional "footer" content to be added.
|
26
|
+
#
|
27
|
+
# <%= Katalyst::Tables::FilterComponent.new(collection: @people, url: peoples_path) do |filter| %>
|
28
|
+
# <% filter.with_modal(collection:) do |modal| %>
|
29
|
+
# <% modal.with_footer do %>
|
30
|
+
# <%= link_to "Docs", docs_path %>
|
31
|
+
# <% end %>
|
32
|
+
# <% end %>
|
33
|
+
# <% end %>
|
34
|
+
#
|
35
|
+
class FilterComponent < ViewComponent::Base
|
36
|
+
include Katalyst::HtmlAttributes
|
37
|
+
include Katalyst::Tables::Frontend
|
38
|
+
|
39
|
+
renders_one :modal, Katalyst::Tables::Filter::ModalComponent
|
40
|
+
|
41
|
+
define_html_attribute_methods :input_attributes
|
42
|
+
|
43
|
+
attr_reader :collection, :url
|
44
|
+
|
45
|
+
def initialize(collection:, url:, **)
|
46
|
+
super(**)
|
47
|
+
|
48
|
+
@collection = collection
|
49
|
+
@url = url
|
50
|
+
end
|
51
|
+
|
52
|
+
def before_render
|
53
|
+
with_modal(collection:) unless modal?
|
54
|
+
end
|
55
|
+
|
56
|
+
def form(url: @url, **options, &)
|
57
|
+
form_with(model: collection,
|
58
|
+
url:,
|
59
|
+
method: :get,
|
60
|
+
**options) do |form|
|
61
|
+
concat(form.hidden_field(:sort))
|
62
|
+
|
63
|
+
yield form if block_given?
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def default_html_attributes
|
70
|
+
{
|
71
|
+
data: {
|
72
|
+
controller: "tables--filter--modal",
|
73
|
+
action: <<~ACTIONS.gsub(/\s+/, " "),
|
74
|
+
click@window->tables--filter--modal#close
|
75
|
+
click->tables--filter--modal#open:stop
|
76
|
+
keydown.esc->tables--filter--modal#close
|
77
|
+
ACTIONS
|
78
|
+
},
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
def default_input_attributes
|
83
|
+
{
|
84
|
+
data: {
|
85
|
+
action: "focus->tables--filter--modal#open",
|
86
|
+
},
|
87
|
+
}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -8,6 +8,7 @@ module Katalyst
|
|
8
8
|
|
9
9
|
included do
|
10
10
|
class_attribute :_default_table_component, instance_accessor: false
|
11
|
+
class_attribute :_default_summary_table_component, instance_accessor: false
|
11
12
|
end
|
12
13
|
|
13
14
|
class_methods do
|
@@ -18,12 +19,26 @@ module Katalyst
|
|
18
19
|
def default_table_component(component)
|
19
20
|
self._default_table_component = component
|
20
21
|
end
|
22
|
+
|
23
|
+
# Set the summary table component to be used as the default for all
|
24
|
+
# summary tables in the views rendered by this controller and its
|
25
|
+
# subclasses.
|
26
|
+
#
|
27
|
+
# @param component [Class] the summary table component class to use
|
28
|
+
def default_summary_table_component(component)
|
29
|
+
self._default_summary_table_component = component
|
30
|
+
end
|
21
31
|
end
|
22
32
|
|
23
33
|
# Default table component for this controller
|
24
34
|
def default_table_component
|
25
35
|
self.class._default_table_component
|
26
36
|
end
|
37
|
+
|
38
|
+
# Default summary table component for this controller
|
39
|
+
def default_summary_table_component
|
40
|
+
self.class._default_summary_table_component
|
41
|
+
end
|
27
42
|
end
|
28
43
|
end
|
29
44
|
end
|
@@ -55,12 +55,39 @@ module Katalyst
|
|
55
55
|
render(Selectable::FormComponent.new(collection:, id:, primary_key:), &)
|
56
56
|
end
|
57
57
|
|
58
|
+
# Construct a new summary table component.
|
59
|
+
#
|
60
|
+
# @param model [ActiveRecord::Base] subject for the table
|
61
|
+
#
|
62
|
+
# Blocks will receive the table in row-rendering mode (with row and record defined):
|
63
|
+
# @yieldparam [Katalyst::TableComponent] the table component to render rows
|
64
|
+
# @yieldparam [nil, Object] nil for the header column, or the given model for the value column
|
65
|
+
def summary_table_with(model:, **, &)
|
66
|
+
component ||= default_summary_table_component_class
|
67
|
+
component = component.new(model:, **)
|
68
|
+
|
69
|
+
render(component, &)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Construct a new filter.
|
73
|
+
#
|
74
|
+
# @param collection [Katalyst::Tables::Collection::Core] the collection to render
|
75
|
+
# @param url [String] the url to submit the form to (e.g. <resources>_path)
|
76
|
+
def filter_with(collection:, url: url_for(action: :index), &)
|
77
|
+
render(FilterComponent.new(collection:, url:), &)
|
78
|
+
end
|
79
|
+
|
58
80
|
private
|
59
81
|
|
60
82
|
def default_table_component_class
|
61
83
|
component = controller.try(:default_table_component) || TableComponent
|
62
84
|
component.respond_to?(:constantize) ? component.constantize : component
|
63
85
|
end
|
86
|
+
|
87
|
+
def default_summary_table_component_class
|
88
|
+
component = controller.try(:default_summary_table_component) || SummaryTableComponent
|
89
|
+
component.respond_to?(:constantize) ? component.constantize : component
|
90
|
+
end
|
64
91
|
end
|
65
92
|
end
|
66
93
|
end
|
@@ -3,6 +3,7 @@ import OrderableListController from "./orderable/list_controller";
|
|
3
3
|
import OrderableFormController from "./orderable/form_controller";
|
4
4
|
import SelectionFormController from "./selection/form_controller";
|
5
5
|
import SelectionItemController from "./selection/item_controller";
|
6
|
+
import FilterModalController from "./filter/modal_controller";
|
6
7
|
|
7
8
|
const Definitions = [
|
8
9
|
{
|
@@ -25,6 +26,10 @@ const Definitions = [
|
|
25
26
|
identifier: "tables--selection--item",
|
26
27
|
controllerConstructor: SelectionItemController,
|
27
28
|
},
|
29
|
+
{
|
30
|
+
identifier: "tables--filter--modal",
|
31
|
+
controllerConstructor: FilterModalController,
|
32
|
+
},
|
28
33
|
];
|
29
34
|
|
30
35
|
export { Definitions as default };
|
@@ -0,0 +1,13 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
|
3
|
+
export default class FilterModalController extends Controller {
|
4
|
+
static targets = ["modal"];
|
5
|
+
|
6
|
+
close(e) {
|
7
|
+
delete this.modalTarget.dataset.open;
|
8
|
+
}
|
9
|
+
|
10
|
+
open(e) {
|
11
|
+
this.modalTarget.dataset.open = "true";
|
12
|
+
}
|
13
|
+
}
|
@@ -25,6 +25,10 @@ module Katalyst
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|
28
|
+
|
29
|
+
def enum_attribute?(key)
|
30
|
+
_default_attributes[key].value.is_a?(::Array)
|
31
|
+
end
|
28
32
|
end
|
29
33
|
|
30
34
|
included do
|
@@ -39,11 +43,21 @@ module Katalyst
|
|
39
43
|
clear_changes_information
|
40
44
|
end
|
41
45
|
|
46
|
+
# Collections are filtered when any parameters have changed from their defaults.
|
47
|
+
def filtered?
|
48
|
+
filters.any?
|
49
|
+
end
|
50
|
+
|
42
51
|
# Collections that do not include Sorting are never sortable.
|
43
52
|
def sortable?
|
44
53
|
false
|
45
54
|
end
|
46
55
|
|
56
|
+
# Collections that do not include Query are never searchable.
|
57
|
+
def searchable?
|
58
|
+
false
|
59
|
+
end
|
60
|
+
|
47
61
|
def apply(items)
|
48
62
|
@items = items
|
49
63
|
reducers.build do |_|
|
@@ -52,6 +66,22 @@ module Katalyst
|
|
52
66
|
end.call(self)
|
53
67
|
self
|
54
68
|
end
|
69
|
+
|
70
|
+
def filter
|
71
|
+
# no-op by default
|
72
|
+
end
|
73
|
+
|
74
|
+
def filters
|
75
|
+
changes.except("sort", "page", "query").transform_values(&:second)
|
76
|
+
end
|
77
|
+
|
78
|
+
def model
|
79
|
+
if items < ActiveRecord::Base
|
80
|
+
items
|
81
|
+
else
|
82
|
+
items.model
|
83
|
+
end
|
84
|
+
end
|
55
85
|
end
|
56
86
|
end
|
57
87
|
end
|
@@ -3,19 +3,90 @@
|
|
3
3
|
module Katalyst
|
4
4
|
module Tables
|
5
5
|
module Collection
|
6
|
-
|
6
|
+
module Filtering
|
7
|
+
extend ActiveSupport::Concern
|
7
8
|
|
8
|
-
|
9
|
-
def filter
|
10
|
-
# no-op by default
|
11
|
-
end
|
9
|
+
DEFAULT_ATTRIBUTES = %w[sort page query].freeze
|
12
10
|
|
13
|
-
|
14
|
-
|
11
|
+
included do
|
12
|
+
use(Filter)
|
15
13
|
end
|
16
14
|
|
17
|
-
|
18
|
-
|
15
|
+
class Filter
|
16
|
+
include ActiveRecord::Sanitization::ClassMethods
|
17
|
+
|
18
|
+
def initialize(app)
|
19
|
+
@app = app
|
20
|
+
end
|
21
|
+
|
22
|
+
def call(collection)
|
23
|
+
collection.class._default_attributes.each_value do |attribute|
|
24
|
+
key = attribute.name
|
25
|
+
|
26
|
+
next if DEFAULT_ATTRIBUTES.include?(key)
|
27
|
+
|
28
|
+
value = collection.attributes[key]
|
29
|
+
|
30
|
+
filter_attribute(collection, key, value, attribute.type.type)
|
31
|
+
end
|
32
|
+
|
33
|
+
@app.call(collection)
|
34
|
+
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
|
19
90
|
end
|
20
91
|
end
|
21
92
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Collection
|
6
|
+
module Query
|
7
|
+
class ArrayValueParser < ValueParser
|
8
|
+
# @param query [StringScanner]
|
9
|
+
def parse(query)
|
10
|
+
@query = query
|
11
|
+
|
12
|
+
skip_whitespace
|
13
|
+
|
14
|
+
if query.scan(/#{'\['}/)
|
15
|
+
take_values
|
16
|
+
else
|
17
|
+
take_value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def take_values
|
22
|
+
until query.eos?
|
23
|
+
skip_whitespace
|
24
|
+
break unless take_quoted_value || take_unquoted_value
|
25
|
+
|
26
|
+
skip_whitespace
|
27
|
+
break unless take_delimiter
|
28
|
+
end
|
29
|
+
|
30
|
+
skip_whitespace
|
31
|
+
take_end_of_list
|
32
|
+
end
|
33
|
+
|
34
|
+
def take_value
|
35
|
+
take_quoted_value || take_unquoted_value
|
36
|
+
end
|
37
|
+
|
38
|
+
def take_delimiter
|
39
|
+
query.scan(/#{','}/)
|
40
|
+
end
|
41
|
+
|
42
|
+
def take_end_of_list
|
43
|
+
query.scan(/#{']'}/)
|
44
|
+
end
|
45
|
+
|
46
|
+
def value=(value)
|
47
|
+
return if @attribute.type_cast(value).nil? # undefined attribute
|
48
|
+
|
49
|
+
current = @collection.attributes[@attribute.name]
|
50
|
+
@collection.assign_attributes(@attribute.name => current + [value])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Collection
|
6
|
+
module Query
|
7
|
+
class Parser # :nodoc:
|
8
|
+
# query [StringScanner]
|
9
|
+
attr_accessor :query
|
10
|
+
attr_reader :collection, :untagged
|
11
|
+
|
12
|
+
def initialize(collection)
|
13
|
+
@collection = collection
|
14
|
+
@untagged = []
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param query [String]
|
18
|
+
def parse(query)
|
19
|
+
@query = StringScanner.new(query)
|
20
|
+
|
21
|
+
until @query.eos?
|
22
|
+
skip_whitespace
|
23
|
+
|
24
|
+
# break to ensure we don't loop indefinitely on bad input
|
25
|
+
break unless take_tagged || take_untagged
|
26
|
+
end
|
27
|
+
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def skip_whitespace
|
34
|
+
query.scan(/\s+/)
|
35
|
+
end
|
36
|
+
|
37
|
+
def take_tagged
|
38
|
+
return unless query.scan(/(\w+(\.\w+)?):/)
|
39
|
+
|
40
|
+
key, = query.values_at(1)
|
41
|
+
parser_for(key).parse(query)
|
42
|
+
end
|
43
|
+
|
44
|
+
def take_untagged
|
45
|
+
return unless query.scan(/\S+/)
|
46
|
+
|
47
|
+
untagged << query.matched
|
48
|
+
|
49
|
+
untagged
|
50
|
+
end
|
51
|
+
|
52
|
+
def parser_for(key)
|
53
|
+
attribute = collection.class._default_attributes[key]
|
54
|
+
|
55
|
+
if collection.class.enum_attribute?(key)
|
56
|
+
ArrayValueParser.new(collection:, attribute:)
|
57
|
+
else
|
58
|
+
SingleValueParser.new(collection:, attribute:)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Collection
|
6
|
+
module Query
|
7
|
+
class SingleValueParser < ValueParser
|
8
|
+
# @param query [StringScanner]
|
9
|
+
def parse(query)
|
10
|
+
@query = query
|
11
|
+
|
12
|
+
take_quoted_value || take_unquoted_value
|
13
|
+
end
|
14
|
+
|
15
|
+
def value=(value)
|
16
|
+
return if @attribute.type_cast(value).nil? # undefined attribute
|
17
|
+
|
18
|
+
@collection.assign_attributes(@attribute.name => value)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Tables
|
5
|
+
module Collection
|
6
|
+
module Query
|
7
|
+
class ValueParser
|
8
|
+
attr_accessor :query
|
9
|
+
|
10
|
+
def initialize(collection:, attribute:)
|
11
|
+
@collection = collection
|
12
|
+
@attribute = attribute
|
13
|
+
end
|
14
|
+
|
15
|
+
def take_quoted_value
|
16
|
+
return unless query.scan(/"([^"]*)"/)
|
17
|
+
|
18
|
+
self.value, = query.values_at(1)
|
19
|
+
end
|
20
|
+
|
21
|
+
def take_unquoted_value
|
22
|
+
return unless query.scan(/([^" \],]*)/)
|
23
|
+
|
24
|
+
self.value, = query.values_at(1)
|
25
|
+
end
|
26
|
+
|
27
|
+
def skip_whitespace
|
28
|
+
query.scan(/\s+/)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|