refine-rails 2.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +413 -0
- data/Rakefile +8 -0
- data/app/assets/config/refine_rails_manifest.js +0 -0
- data/app/assets/javascripts/refine-stimulus.esm.js +2 -0
- data/app/assets/javascripts/refine-stimulus.esm.js.map +1 -0
- data/app/assets/javascripts/refine-stimulus.js +2 -0
- data/app/assets/javascripts/refine-stimulus.js.map +1 -0
- data/app/assets/javascripts/refine-stimulus.modern.js +2 -0
- data/app/assets/javascripts/refine-stimulus.modern.js.map +1 -0
- data/app/assets/javascripts/refine-stimulus.umd.js +2 -0
- data/app/assets/javascripts/refine-stimulus.umd.js.map +1 -0
- data/app/assets/stylesheets/index.css +1873 -0
- data/app/assets/stylesheets/index.tailwind.css +1035 -0
- data/app/controllers/refine/blueprints_controller.rb +80 -0
- data/app/controllers/refine/filter_application_controller.rb +29 -0
- data/app/controllers/refine/inline/criteria_controller.rb +161 -0
- data/app/controllers/refine/inline/stored_filters_controller.rb +84 -0
- data/app/controllers/refine/stored_filters_controller.rb +69 -0
- data/app/javascript/controllers/index.js +66 -0
- data/app/javascript/controllers/refine/add-controller.js +42 -0
- data/app/javascript/controllers/refine/criterion-form-controller.js +31 -0
- data/app/javascript/controllers/refine/date-controller.js +113 -0
- data/app/javascript/controllers/refine/defaults-controller.js +32 -0
- data/app/javascript/controllers/refine/delete-controller.js +13 -0
- data/app/javascript/controllers/refine/filter-pills-controller.js +63 -0
- data/app/javascript/controllers/refine/form-controller.js +51 -0
- data/app/javascript/controllers/refine/inline-conditions-controller.js +33 -0
- data/app/javascript/controllers/refine/popup-controller.js +46 -0
- data/app/javascript/controllers/refine/search-filter-controller.js +50 -0
- data/app/javascript/controllers/refine/server-refresh-controller.js +43 -0
- data/app/javascript/controllers/refine/state-controller.js +220 -0
- data/app/javascript/controllers/refine/stored-filter-controller.js +23 -0
- data/app/javascript/controllers/refine/submit-form-controller.js +8 -0
- data/app/javascript/controllers/refine/toggle-controller.js +12 -0
- data/app/javascript/controllers/refine/turbo-stream-form-controller.js +24 -0
- data/app/javascript/controllers/refine/turbo-stream-link-controller.js +24 -0
- data/app/javascript/controllers/refine/update-controller.js +86 -0
- data/app/javascript/index.js +1 -0
- data/app/javascript/refine/helpers/index.js +77 -0
- data/app/models/refine/blueprints/blueprint.rb +58 -0
- data/app/models/refine/blueprints/blueprint_example.json +25 -0
- data/app/models/refine/conditions/boolean_condition.rb +112 -0
- data/app/models/refine/conditions/clause.rb +38 -0
- data/app/models/refine/conditions/clauses.rb +38 -0
- data/app/models/refine/conditions/condition.rb +285 -0
- data/app/models/refine/conditions/condition_error.rb +1 -0
- data/app/models/refine/conditions/date_condition.rb +464 -0
- data/app/models/refine/conditions/date_with_time_condition.rb +8 -0
- data/app/models/refine/conditions/errors/condition_clause_error.rb +7 -0
- data/app/models/refine/conditions/errors/criteria_limit_exceeded_error.rb +2 -0
- data/app/models/refine/conditions/errors/option_error.rb +2 -0
- data/app/models/refine/conditions/errors/relationship_error.rb +1 -0
- data/app/models/refine/conditions/filter_condition.rb +93 -0
- data/app/models/refine/conditions/has_clauses.rb +117 -0
- data/app/models/refine/conditions/has_meta.rb +10 -0
- data/app/models/refine/conditions/has_refinements.rb +156 -0
- data/app/models/refine/conditions/numeric_condition.rb +224 -0
- data/app/models/refine/conditions/option_condition.rb +260 -0
- data/app/models/refine/conditions/text_condition.rb +152 -0
- data/app/models/refine/conditions/uses_attributes.rb +168 -0
- data/app/models/refine/filter.rb +302 -0
- data/app/models/refine/filters/blueprint_editor.rb +102 -0
- data/app/models/refine/filters/builder.rb +59 -0
- data/app/models/refine/filters/criterion.rb +87 -0
- data/app/models/refine/filters/query.rb +82 -0
- data/app/models/refine/inline/criteria/input.rb +50 -0
- data/app/models/refine/inline/criteria/numeric_refinement.rb +13 -0
- data/app/models/refine/inline/criteria/option.rb +2 -0
- data/app/models/refine/inline/criterion.rb +141 -0
- data/app/models/refine/invalid_filter_error.rb +8 -0
- data/app/models/refine/stabilize.rb +29 -0
- data/app/models/refine/stabilizers/database_stabilizer.rb +21 -0
- data/app/models/refine/stabilizers/errors/url_stabilizer_error.rb +2 -0
- data/app/models/refine/stabilizers/url_encoded_stabilizer.rb +21 -0
- data/app/models/refine/stored_filter.rb +14 -0
- data/app/models/refine/tracks_pending_relationship_subqueries.rb +196 -0
- data/app/views/_filter_builder_dropdown.html.erb +63 -0
- data/app/views/_filter_pills.html.erb +40 -0
- data/app/views/_loading.html.erb +32 -0
- data/app/views/refine/blueprints/_add_and.html.erb +25 -0
- data/app/views/refine/blueprints/_add_group.html.erb +24 -0
- data/app/views/refine/blueprints/_clause_select.html.erb +24 -0
- data/app/views/refine/blueprints/_condition_select.html.erb +53 -0
- data/app/views/refine/blueprints/_criterion.html.erb +41 -0
- data/app/views/refine/blueprints/_criterion_errors.html.erb +7 -0
- data/app/views/refine/blueprints/_delete_criterion.html.erb +11 -0
- data/app/views/refine/blueprints/_group.html.erb +13 -0
- data/app/views/refine/blueprints/_query.html.erb +34 -0
- data/app/views/refine/blueprints/_stored_filters.html.erb +23 -0
- data/app/views/refine/blueprints/clauses/_date_condition.html.erb +80 -0
- data/app/views/refine/blueprints/clauses/_date_picker.html.erb +26 -0
- data/app/views/refine/blueprints/clauses/_filter_condition.html.erb +36 -0
- data/app/views/refine/blueprints/clauses/_numeric_condition.html.erb +35 -0
- data/app/views/refine/blueprints/clauses/_option_condition.html.erb +37 -0
- data/app/views/refine/blueprints/clauses/_text_condition.html.erb +13 -0
- data/app/views/refine/blueprints/create.turbo_stream.erb +22 -0
- data/app/views/refine/blueprints/new.html.erb +7 -0
- data/app/views/refine/blueprints/show.html.erb +4 -0
- data/app/views/refine/blueprints/show.turbo_stream.erb +22 -0
- data/app/views/refine/inline/criteria/_form_fields.html.erb +62 -0
- data/app/views/refine/inline/criteria/create.turbo_stream.erb +19 -0
- data/app/views/refine/inline/criteria/edit.turbo_stream.erb +26 -0
- data/app/views/refine/inline/criteria/index.html.erb +64 -0
- data/app/views/refine/inline/criteria/new.turbo_stream.erb +24 -0
- data/app/views/refine/inline/filters/_add_first_condition_button.html.erb +19 -0
- data/app/views/refine/inline/filters/_and_button.html.erb +26 -0
- data/app/views/refine/inline/filters/_criterion.html.erb +23 -0
- data/app/views/refine/inline/filters/_group.html.erb +13 -0
- data/app/views/refine/inline/filters/_load_button.html.erb +15 -0
- data/app/views/refine/inline/filters/_or_button.html.erb +26 -0
- data/app/views/refine/inline/filters/_popup.html.erb +26 -0
- data/app/views/refine/inline/filters/_save_button.html.erb +15 -0
- data/app/views/refine/inline/filters/_show.html.erb +40 -0
- data/app/views/refine/inline/inputs/_date_condition.html.erb +7 -0
- data/app/views/refine/inline/inputs/_date_condition_days.html.erb +18 -0
- data/app/views/refine/inline/inputs/_date_condition_range.html.erb +22 -0
- data/app/views/refine/inline/inputs/_date_condition_single.html.erb +9 -0
- data/app/views/refine/inline/inputs/_date_picker.html.erb +20 -0
- data/app/views/refine/inline/inputs/_numeric_condition.html.erb +23 -0
- data/app/views/refine/inline/inputs/_option_condition.html.erb +14 -0
- data/app/views/refine/inline/inputs/_text_condition.html.erb +8 -0
- data/app/views/refine/inline/stored_filters/find.turbo_stream.erb +19 -0
- data/app/views/refine/inline/stored_filters/index.html.erb +28 -0
- data/app/views/refine/inline/stored_filters/new.turbo_stream.erb +47 -0
- data/app/views/refine/stored_filters/create.turbo_stream.erb +2 -0
- data/app/views/refine/stored_filters/find.turbo_stream.erb +5 -0
- data/app/views/refine/stored_filters/index.html.erb +39 -0
- data/app/views/refine/stored_filters/new.html.erb +29 -0
- data/app/views/refine/stored_filters/show.html.erb +1 -0
- data/config/locales/en/dates.en.yml +29 -0
- data/config/locales/en/en.yml +20 -0
- data/config/locales/en/refine.en.yml +187 -0
- data/config/routes.rb +17 -0
- data/lib/generators/filter/filter_generator.rb +27 -0
- data/lib/generators/filter/templates/filter.rb.erb +20 -0
- data/lib/refine/rails/engine.rb +15 -0
- data/lib/refine/rails/version.rb +5 -0
- data/lib/refine/rails.rb +38 -0
- data/lib/tasks/refine/rails_tasks.rake +13 -0
- metadata +202 -0
@@ -0,0 +1,102 @@
|
|
1
|
+
class Refine::Filters::BlueprintEditor
|
2
|
+
# Editor that will transform a given blueprint IN PLACE
|
3
|
+
# Designed to support operations for the v2 inline filter builder via ConditionsController
|
4
|
+
|
5
|
+
attr_reader :blueprint
|
6
|
+
|
7
|
+
INPUT_ATTRS = %i[
|
8
|
+
clause
|
9
|
+
date1
|
10
|
+
date2
|
11
|
+
days
|
12
|
+
modifier
|
13
|
+
selected
|
14
|
+
value
|
15
|
+
value1
|
16
|
+
value2
|
17
|
+
count_refinement
|
18
|
+
].freeze
|
19
|
+
|
20
|
+
def initialize(blueprint)
|
21
|
+
@blueprint = blueprint
|
22
|
+
end
|
23
|
+
|
24
|
+
# add a criteria at the specified position (defaults to the end)
|
25
|
+
def add(position: -1, conjunction: "and", criterion:)
|
26
|
+
# TODO support multiple input conditions, refinements, etc
|
27
|
+
|
28
|
+
conjunction_depth = case conjunction
|
29
|
+
in "and" then 1
|
30
|
+
in "or" then 0
|
31
|
+
end
|
32
|
+
|
33
|
+
# extract data from criterion
|
34
|
+
condition_id = criterion[:condition_id]
|
35
|
+
input = criterion[:input].slice(*INPUT_ATTRS)
|
36
|
+
|
37
|
+
nodes_to_insert = []
|
38
|
+
nodes_to_insert << {
|
39
|
+
depth: conjunction_depth,
|
40
|
+
type: "conjunction",
|
41
|
+
word: conjunction,
|
42
|
+
} if blueprint[(position.negative? ? position + 1 : position - 1)]
|
43
|
+
|
44
|
+
nodes_to_insert << {
|
45
|
+
depth: 1,
|
46
|
+
type: "criterion",
|
47
|
+
condition_id: condition_id,
|
48
|
+
input: input
|
49
|
+
}
|
50
|
+
|
51
|
+
blueprint.insert position, *nodes_to_insert
|
52
|
+
end
|
53
|
+
|
54
|
+
def update(index, criterion:)
|
55
|
+
# extract data from criterion
|
56
|
+
input = criterion[:input].slice(*INPUT_ATTRS)
|
57
|
+
|
58
|
+
blueprint[index][:input] = input
|
59
|
+
end
|
60
|
+
|
61
|
+
def delete(index)
|
62
|
+
# To support 'groups' there is some complicated logic for deleting criterion.
|
63
|
+
#
|
64
|
+
# Imagine this simplified blueprint: [eq, and, sw, or, eq]
|
65
|
+
#
|
66
|
+
# User clicks to delete the last eq. We also have to delete the preceding or
|
67
|
+
# otherwise we're left with a hanging empty group
|
68
|
+
#
|
69
|
+
# What if the user deletes the sw? We have to clean up the preceding and.
|
70
|
+
#
|
71
|
+
# Imagine another scenario: [eq or sw and ew]
|
72
|
+
# Now we delete the first eq but this time we need to clean up the or.
|
73
|
+
#
|
74
|
+
# These conditionals cover these cases.
|
75
|
+
|
76
|
+
previous_entry = index.zero? ? nil : blueprint[index - 1]
|
77
|
+
next_entry = blueprint[index + 1]
|
78
|
+
|
79
|
+
next_is_or = next_entry && next_entry[:word] == 'or'
|
80
|
+
previous_is_or = previous_entry && previous_entry[:word] == 'or'
|
81
|
+
|
82
|
+
next_is_right_paren = next_is_or || !next_entry
|
83
|
+
previous_is_left_paren = previous_is_or || !previous_entry
|
84
|
+
|
85
|
+
is_first_in_group = previous_is_left_paren && !next_is_right_paren
|
86
|
+
is_last_in_group = previous_is_left_paren && next_is_right_paren
|
87
|
+
is_last_criterion = !previous_entry && !next_entry
|
88
|
+
|
89
|
+
if is_last_criterion
|
90
|
+
blueprint.slice!(index)
|
91
|
+
elsif is_last_in_group && previous_is_or
|
92
|
+
blueprint.slice!((index - 1)..index)
|
93
|
+
elsif is_last_in_group && !previous_entry
|
94
|
+
blueprint.slice!(index..(index + 1))
|
95
|
+
elsif is_first_in_group
|
96
|
+
blueprint.slice!(index..(index + 1))
|
97
|
+
else
|
98
|
+
blueprint.slice!((index - 1)..index)
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
class Refine::Filters::Builder
|
2
|
+
# View Model that holds all aspects of the client state related to the filter builder
|
3
|
+
# This includes auxiliary state like client_id and selected stored filter id
|
4
|
+
#
|
5
|
+
# State for the main query builder form is held in #query which is an instance of
|
6
|
+
# Refine::Filters::Query
|
7
|
+
include ActiveModel::Model
|
8
|
+
|
9
|
+
attr_accessor :blueprint_json,
|
10
|
+
:filter_class,
|
11
|
+
:stable_id,
|
12
|
+
:stored_filter_id,
|
13
|
+
:client_id,
|
14
|
+
:initial_query
|
15
|
+
|
16
|
+
attr_reader :blueprint, :refine_filter, :query
|
17
|
+
|
18
|
+
def initialize(attrs = {})
|
19
|
+
super
|
20
|
+
@client_id ||= SecureRandom.uuid
|
21
|
+
set_refine_filter_and_blueprint!
|
22
|
+
set_query!
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_params
|
26
|
+
{
|
27
|
+
refine_filters_builder: {
|
28
|
+
blueprint_json: blueprint.to_json,
|
29
|
+
stable_id: stable_id,
|
30
|
+
stored_filter_id: stored_filter_id,
|
31
|
+
client_id: client_id,
|
32
|
+
filter_class: filter_class
|
33
|
+
}
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
# For use with the Rails dom_id helper
|
38
|
+
def to_key
|
39
|
+
[client_id]
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def set_refine_filter_and_blueprint!
|
45
|
+
if stable_id.present?
|
46
|
+
@refine_filter = Refine::Rails.configuration.stabilizer_classes[:url].new.from_stable_id(id: stable_id, initial_query: initial_query)
|
47
|
+
@blueprint = @refine_filter.blueprint
|
48
|
+
else
|
49
|
+
json = blueprint_json || "[]"
|
50
|
+
@blueprint = JSON.parse(json).map(&:deep_symbolize_keys)
|
51
|
+
@refine_filter = filter_class.constantize.new(blueprint, initial_query)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def set_query!
|
56
|
+
@query = Refine::Filters::Query.new(@refine_filter)
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
class Refine::Filters::Criterion
|
2
|
+
# View model that holds the state of individual criteria within the Filter query
|
3
|
+
include ActiveModel::Model
|
4
|
+
include ActiveModel::Attributes
|
5
|
+
|
6
|
+
attribute :depth, :integer
|
7
|
+
attribute :criterion, :string
|
8
|
+
attribute :condition_id, :string
|
9
|
+
attribute :input
|
10
|
+
attribute :word, :string
|
11
|
+
attribute :type, :string
|
12
|
+
attribute :position, :integer
|
13
|
+
attribute :uid, :string
|
14
|
+
|
15
|
+
attr_accessor :query
|
16
|
+
delegate :filter, to: :query, allow_nil: true
|
17
|
+
|
18
|
+
attr_reader :condition
|
19
|
+
|
20
|
+
def initialize(**attrs)
|
21
|
+
super
|
22
|
+
initialize_condition!
|
23
|
+
end
|
24
|
+
|
25
|
+
def validate!
|
26
|
+
return if (input&.has_key?(:count_refinement) || input&.has_key?(:date_refinement))
|
27
|
+
errors.clear
|
28
|
+
return true if type == "conjunction"
|
29
|
+
begin
|
30
|
+
query_for_validate = filter.initial_query || filter.model.all
|
31
|
+
condition&.apply(input, filter.table, query_for_validate)
|
32
|
+
rescue Refine::Conditions::Errors::ConditionClauseError => e
|
33
|
+
e.errors.each do |error|
|
34
|
+
errors.add(:base, error.full_message)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def condition_attributes
|
40
|
+
result = condition&.to_array(allow_errors: true)
|
41
|
+
result.to_h
|
42
|
+
end
|
43
|
+
|
44
|
+
def meta
|
45
|
+
condition_attributes[:meta]
|
46
|
+
end
|
47
|
+
|
48
|
+
def selected_clause
|
49
|
+
input[:clause]
|
50
|
+
end
|
51
|
+
|
52
|
+
def selected_clause_meta
|
53
|
+
meta[:clauses].find {|c| c[:id] == selected_clause }[:meta]
|
54
|
+
end
|
55
|
+
|
56
|
+
def refinements
|
57
|
+
condition.refinements_to_array
|
58
|
+
end
|
59
|
+
|
60
|
+
def meta_for_refinement_clause(refinement)
|
61
|
+
refinement_meta = refinement[:meta]
|
62
|
+
selected_clause_id = input[refinement[:id].to_sym][:clause]
|
63
|
+
clauses = refinement_meta[:clauses]
|
64
|
+
selected_clause = clauses.find { |clause| clause[:id] == selected_clause_id }
|
65
|
+
selected_clause[:meta]
|
66
|
+
end
|
67
|
+
|
68
|
+
def component
|
69
|
+
condition.component.underscore
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def initialize_condition!
|
75
|
+
@condition = query
|
76
|
+
.available_conditions
|
77
|
+
.find { |condition| condition.id == condition_id }
|
78
|
+
.dup
|
79
|
+
|
80
|
+
if @condition
|
81
|
+
@condition.set_filter(filter)
|
82
|
+
filter.translate_display(@condition)
|
83
|
+
elsif type != "conjunction"
|
84
|
+
raise Refine::InvalidFilterError
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
class Refine::Filters::Query
|
2
|
+
include ActiveModel::Model
|
3
|
+
# View Model for the main section of the filter builder- the dynamic query form
|
4
|
+
|
5
|
+
attr_reader :filter
|
6
|
+
|
7
|
+
def initialize(filter)
|
8
|
+
@filter = filter
|
9
|
+
add_criteria!
|
10
|
+
end
|
11
|
+
|
12
|
+
def validate!
|
13
|
+
@criteria.each(&:validate!)
|
14
|
+
end
|
15
|
+
|
16
|
+
def valid?
|
17
|
+
validate!
|
18
|
+
@criteria.all? { |c| c.errors.empty? }
|
19
|
+
end
|
20
|
+
|
21
|
+
def clear_errors
|
22
|
+
@criteria.each { |c| c.errors.clear }
|
23
|
+
end
|
24
|
+
|
25
|
+
def grouped_criteria
|
26
|
+
# Allow for an empty blueprint
|
27
|
+
return [] if @criteria.blank?
|
28
|
+
[].tap do |result|
|
29
|
+
# start with an empty group
|
30
|
+
result.push []
|
31
|
+
@criteria.each_with_index do |criterion, index|
|
32
|
+
case criterion.word
|
33
|
+
when "or"
|
34
|
+
result.push []
|
35
|
+
when "and"
|
36
|
+
next
|
37
|
+
else
|
38
|
+
result.last.push criterion
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def available_conditions
|
45
|
+
@filter.conditions
|
46
|
+
end
|
47
|
+
|
48
|
+
def blueprint
|
49
|
+
if @filter.blueprint&.any?
|
50
|
+
@filter.blueprint
|
51
|
+
else
|
52
|
+
[]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def configuration
|
57
|
+
@filter.configuration
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
# The json representation of conditions that is sent to the front end.
|
62
|
+
def available_conditions_attributes
|
63
|
+
configuration[:conditions]
|
64
|
+
end
|
65
|
+
|
66
|
+
# TODO make this a more useful data with pointers to which criteria
|
67
|
+
# each error goes with
|
68
|
+
def error_messages
|
69
|
+
@criteria.flat_map {|c| c.errors.full_messages }
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def add_criteria!
|
75
|
+
@criteria = []
|
76
|
+
blueprint.each.with_index do |criterion_attrs, index|
|
77
|
+
@criteria << Refine::Filters::Criterion.new(
|
78
|
+
**criterion_attrs.merge(query: self, uid: index, position: index)
|
79
|
+
)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class Refine::Inline::Criteria::Input
|
2
|
+
include ActiveModel::Model
|
3
|
+
|
4
|
+
MODIFIERS = {
|
5
|
+
ago: "days ago",
|
6
|
+
from_now: "days from now"
|
7
|
+
}.freeze
|
8
|
+
|
9
|
+
attr_accessor :clause,
|
10
|
+
:date1,
|
11
|
+
:date2,
|
12
|
+
:days,
|
13
|
+
:modifier,
|
14
|
+
:selected,
|
15
|
+
:value,
|
16
|
+
:value1,
|
17
|
+
:value2,
|
18
|
+
:count_refinement
|
19
|
+
|
20
|
+
def attributes
|
21
|
+
{
|
22
|
+
clause: clause,
|
23
|
+
date1: date1,
|
24
|
+
date2: date2,
|
25
|
+
days: days,
|
26
|
+
modifier: modifier,
|
27
|
+
selected: selected,
|
28
|
+
value: value,
|
29
|
+
value1: value1,
|
30
|
+
value2: value2,
|
31
|
+
count_refinement_attributes: count_refinement_attributes.presence
|
32
|
+
}.compact
|
33
|
+
end
|
34
|
+
|
35
|
+
def count_refinement
|
36
|
+
@count_refinement ||= Refine::Inline::Criteria::NumericRefinement.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def count_refinement_attributes
|
40
|
+
count_refinement.attributes
|
41
|
+
end
|
42
|
+
|
43
|
+
def count_refinement_attributes=(attrs = {})
|
44
|
+
count_refinement.attributes = attrs.to_h
|
45
|
+
end
|
46
|
+
|
47
|
+
def selected=(value)
|
48
|
+
@selected = Array.wrap(value)
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
class Refine::Inline::Criterion
|
2
|
+
include ActiveModel::Model
|
3
|
+
|
4
|
+
attr_accessor :stable_id,
|
5
|
+
:client_id,
|
6
|
+
:condition_id,
|
7
|
+
:input,
|
8
|
+
:position,
|
9
|
+
:conjunction,
|
10
|
+
:refine_filter
|
11
|
+
|
12
|
+
# initialize a Crtierion object from a blueprint node
|
13
|
+
def self.from_blueprint_node(node, **additional_attrs)
|
14
|
+
attrs = node.deep_dup.merge(additional_attrs)
|
15
|
+
|
16
|
+
# delete some attributes we don't need
|
17
|
+
attrs.delete(:depth)
|
18
|
+
attrs.delete(:type)
|
19
|
+
|
20
|
+
# suffix nested hash keys with '_attributes' to initialize objects
|
21
|
+
attrs[:input_attributes] = attrs.delete(:input)
|
22
|
+
if input_attrs = attrs[:input_attributes]
|
23
|
+
input_attrs[:count_refinement_attributes] = input_attrs.delete(:count_refinement)
|
24
|
+
end
|
25
|
+
new(attrs)
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Returns a nested array of Criterion objects reflecting the grouping of the OR groups in a filter's blueprint
|
30
|
+
def self.groups_from_filter(refine_filter, **attrs)
|
31
|
+
return [] unless refine_filter&.blueprint.present?
|
32
|
+
[].tap do |result|
|
33
|
+
result.push([])
|
34
|
+
refine_filter.blueprint.each_with_index do |node, i|
|
35
|
+
case node[:word]
|
36
|
+
when "or"
|
37
|
+
result.push []
|
38
|
+
when "and"
|
39
|
+
next
|
40
|
+
else
|
41
|
+
criterion = from_blueprint_node(node, **attrs.merge(refine_filter: refine_filter, position: i))
|
42
|
+
result.last.push criterion
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def attributes
|
49
|
+
{
|
50
|
+
stable_id: stable_id,
|
51
|
+
client_id: client_id,
|
52
|
+
condition_id: condition_id,
|
53
|
+
position: position,
|
54
|
+
conjunction: conjunction,
|
55
|
+
input_attributes: input_attributes
|
56
|
+
}.compact
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_params
|
60
|
+
{refine_inline_criterion: attributes}
|
61
|
+
end
|
62
|
+
|
63
|
+
def input
|
64
|
+
@input ||= Refine::Inline::Criteria::Input.new
|
65
|
+
end
|
66
|
+
|
67
|
+
def input_attributes
|
68
|
+
input.attributes
|
69
|
+
end
|
70
|
+
|
71
|
+
def input_attributes=(attrs = {})
|
72
|
+
input.attributes = attrs.to_h
|
73
|
+
end
|
74
|
+
|
75
|
+
def to_key
|
76
|
+
[client_id, position, conjunction].map(&:presence).compact
|
77
|
+
end
|
78
|
+
|
79
|
+
def condition
|
80
|
+
@condition ||= begin
|
81
|
+
@refine_filter
|
82
|
+
.instantiated_conditions
|
83
|
+
.find { |c| c.id == condition_id }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def input_partial
|
88
|
+
"refine/inline/inputs/#{condition.component}".underscore
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_blueprint_node
|
92
|
+
result = attributes.slice(:condition_id, :input_attributes)
|
93
|
+
result[:input] = result.delete(:input_attributes)
|
94
|
+
if input_attrs = result[:input]
|
95
|
+
input_attrs[:count_refinement] = input_attrs.delete(:count_refinement_attributes)
|
96
|
+
end
|
97
|
+
result
|
98
|
+
end
|
99
|
+
|
100
|
+
def human_readable_value
|
101
|
+
condition.human_readable_value(to_blueprint_node[:input])
|
102
|
+
end
|
103
|
+
|
104
|
+
def condition_display
|
105
|
+
condition&.display
|
106
|
+
end
|
107
|
+
|
108
|
+
def clause_display
|
109
|
+
condition&.clause_display(input&.clause)
|
110
|
+
end
|
111
|
+
|
112
|
+
def options
|
113
|
+
if condition.respond_to? :get_options
|
114
|
+
condition.get_options.call.map {|option_hash| Refine::Inline::Criteria::Option.new(**option_hash)}
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def multiple?
|
119
|
+
selected_clause = condition.clauses.detect {|c| c.id == input.clause } || condition.clauses.first
|
120
|
+
selected_clause.meta[:multiple].present?
|
121
|
+
end
|
122
|
+
|
123
|
+
def validate!
|
124
|
+
# TODO figure out how to validate inputs with count refinements
|
125
|
+
return if input.count_refinement.attributes.present?
|
126
|
+
errors.clear
|
127
|
+
begin
|
128
|
+
query_for_validate = refine_filter.initial_query || refine_filter.model.all
|
129
|
+
condition&.apply(input_attributes, refine_filter.table, query_for_validate)
|
130
|
+
rescue Refine::Conditions::Errors::ConditionClauseError => e
|
131
|
+
e.errors.each do |error|
|
132
|
+
errors.add(:base, error.full_message)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def valid?
|
138
|
+
validate!
|
139
|
+
errors.empty?
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Refine
|
2
|
+
module Stabilize
|
3
|
+
|
4
|
+
def automatically_stabilize?
|
5
|
+
false
|
6
|
+
end
|
7
|
+
|
8
|
+
def automatic_stable_id_generator
|
9
|
+
self.class.default_stabilizer
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_optional_stable_id(stabilizer=nil)
|
13
|
+
create_stable_id(stabilizer) if automatically_stabilize?
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_stable_id(stabilizer=nil)
|
17
|
+
make_stable_id_generator(stabilizer).new.to_stable_id(filter: self)
|
18
|
+
end
|
19
|
+
|
20
|
+
def make_stable_id_generator(stabilizer = nil)
|
21
|
+
generator = stabilizer || automatic_stable_id_generator
|
22
|
+
if generator.blank?
|
23
|
+
raise ArgumentError.new('No stable id class set. Set using the default_stable_id_generator method')
|
24
|
+
end
|
25
|
+
generator
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Refine
|
2
|
+
module Stabilizers
|
3
|
+
class DatabaseStabilizer
|
4
|
+
|
5
|
+
def to_stable_id(filter:, name: nil)
|
6
|
+
# Serialize the filter class and blueprint. Reference via id.
|
7
|
+
model.find_or_create_by!(state: filter.state, name: name).id
|
8
|
+
end
|
9
|
+
|
10
|
+
def from_stable_id(id:, initial_query: nil)
|
11
|
+
# Find the associated StoredFilter by id and return state. Decode to create blueprint
|
12
|
+
state = ActiveSupport::JSON.decode(model.find(id).state).deep_symbolize_keys
|
13
|
+
Refine::Filter.from_state(state, initial_query)
|
14
|
+
end
|
15
|
+
|
16
|
+
def model
|
17
|
+
Refine::StoredFilter
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Refine
|
2
|
+
module Stabilizers
|
3
|
+
class UrlEncodedStabilizer
|
4
|
+
|
5
|
+
def to_stable_id(filter:)
|
6
|
+
compressed_state = ActiveSupport::Gzip.compress(filter.state)
|
7
|
+
encoded_state = Base64.encode64(compressed_state)
|
8
|
+
CGI.escape(encoded_state)
|
9
|
+
end
|
10
|
+
|
11
|
+
def from_stable_id(id:, initial_query: nil)
|
12
|
+
raise Refine::Stabilizers::Errors::UrlStabilizerError if id.blank?
|
13
|
+
url_decoded = CGI.unescape(id)
|
14
|
+
base_64_decoded = Base64.decode64(url_decoded)
|
15
|
+
uncompress = ActiveSupport::Gzip.decompress(base_64_decoded)
|
16
|
+
state = ActiveSupport::JSON.decode(uncompress).deep_symbolize_keys
|
17
|
+
Refine::Filter.from_state(state, initial_query)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Refine
|
2
|
+
class StoredFilter < ApplicationRecord
|
3
|
+
validates_presence_of :state
|
4
|
+
self.table_name = "refine_stored_filters"
|
5
|
+
|
6
|
+
def refine_filter(initial_query: nil)
|
7
|
+
Refine::Rails.configuration.stabilizer_classes[:db].new.from_stable_id(id: id, initial_query: initial_query)
|
8
|
+
end
|
9
|
+
|
10
|
+
def blueprint
|
11
|
+
JSON.parse(state)["blueprint"]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|