refine-rails 2.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (141) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +413 -0
  3. data/Rakefile +8 -0
  4. data/app/assets/config/refine_rails_manifest.js +0 -0
  5. data/app/assets/javascripts/refine-stimulus.esm.js +2 -0
  6. data/app/assets/javascripts/refine-stimulus.esm.js.map +1 -0
  7. data/app/assets/javascripts/refine-stimulus.js +2 -0
  8. data/app/assets/javascripts/refine-stimulus.js.map +1 -0
  9. data/app/assets/javascripts/refine-stimulus.modern.js +2 -0
  10. data/app/assets/javascripts/refine-stimulus.modern.js.map +1 -0
  11. data/app/assets/javascripts/refine-stimulus.umd.js +2 -0
  12. data/app/assets/javascripts/refine-stimulus.umd.js.map +1 -0
  13. data/app/assets/stylesheets/index.css +1873 -0
  14. data/app/assets/stylesheets/index.tailwind.css +1035 -0
  15. data/app/controllers/refine/blueprints_controller.rb +80 -0
  16. data/app/controllers/refine/filter_application_controller.rb +29 -0
  17. data/app/controllers/refine/inline/criteria_controller.rb +161 -0
  18. data/app/controllers/refine/inline/stored_filters_controller.rb +84 -0
  19. data/app/controllers/refine/stored_filters_controller.rb +69 -0
  20. data/app/javascript/controllers/index.js +66 -0
  21. data/app/javascript/controllers/refine/add-controller.js +42 -0
  22. data/app/javascript/controllers/refine/criterion-form-controller.js +31 -0
  23. data/app/javascript/controllers/refine/date-controller.js +113 -0
  24. data/app/javascript/controllers/refine/defaults-controller.js +32 -0
  25. data/app/javascript/controllers/refine/delete-controller.js +13 -0
  26. data/app/javascript/controllers/refine/filter-pills-controller.js +63 -0
  27. data/app/javascript/controllers/refine/form-controller.js +51 -0
  28. data/app/javascript/controllers/refine/inline-conditions-controller.js +33 -0
  29. data/app/javascript/controllers/refine/popup-controller.js +46 -0
  30. data/app/javascript/controllers/refine/search-filter-controller.js +50 -0
  31. data/app/javascript/controllers/refine/server-refresh-controller.js +43 -0
  32. data/app/javascript/controllers/refine/state-controller.js +220 -0
  33. data/app/javascript/controllers/refine/stored-filter-controller.js +23 -0
  34. data/app/javascript/controllers/refine/submit-form-controller.js +8 -0
  35. data/app/javascript/controllers/refine/toggle-controller.js +12 -0
  36. data/app/javascript/controllers/refine/turbo-stream-form-controller.js +24 -0
  37. data/app/javascript/controllers/refine/turbo-stream-link-controller.js +24 -0
  38. data/app/javascript/controllers/refine/update-controller.js +86 -0
  39. data/app/javascript/index.js +1 -0
  40. data/app/javascript/refine/helpers/index.js +77 -0
  41. data/app/models/refine/blueprints/blueprint.rb +58 -0
  42. data/app/models/refine/blueprints/blueprint_example.json +25 -0
  43. data/app/models/refine/conditions/boolean_condition.rb +112 -0
  44. data/app/models/refine/conditions/clause.rb +38 -0
  45. data/app/models/refine/conditions/clauses.rb +38 -0
  46. data/app/models/refine/conditions/condition.rb +285 -0
  47. data/app/models/refine/conditions/condition_error.rb +1 -0
  48. data/app/models/refine/conditions/date_condition.rb +464 -0
  49. data/app/models/refine/conditions/date_with_time_condition.rb +8 -0
  50. data/app/models/refine/conditions/errors/condition_clause_error.rb +7 -0
  51. data/app/models/refine/conditions/errors/criteria_limit_exceeded_error.rb +2 -0
  52. data/app/models/refine/conditions/errors/option_error.rb +2 -0
  53. data/app/models/refine/conditions/errors/relationship_error.rb +1 -0
  54. data/app/models/refine/conditions/filter_condition.rb +93 -0
  55. data/app/models/refine/conditions/has_clauses.rb +117 -0
  56. data/app/models/refine/conditions/has_meta.rb +10 -0
  57. data/app/models/refine/conditions/has_refinements.rb +156 -0
  58. data/app/models/refine/conditions/numeric_condition.rb +224 -0
  59. data/app/models/refine/conditions/option_condition.rb +260 -0
  60. data/app/models/refine/conditions/text_condition.rb +152 -0
  61. data/app/models/refine/conditions/uses_attributes.rb +168 -0
  62. data/app/models/refine/filter.rb +302 -0
  63. data/app/models/refine/filters/blueprint_editor.rb +102 -0
  64. data/app/models/refine/filters/builder.rb +59 -0
  65. data/app/models/refine/filters/criterion.rb +87 -0
  66. data/app/models/refine/filters/query.rb +82 -0
  67. data/app/models/refine/inline/criteria/input.rb +50 -0
  68. data/app/models/refine/inline/criteria/numeric_refinement.rb +13 -0
  69. data/app/models/refine/inline/criteria/option.rb +2 -0
  70. data/app/models/refine/inline/criterion.rb +141 -0
  71. data/app/models/refine/invalid_filter_error.rb +8 -0
  72. data/app/models/refine/stabilize.rb +29 -0
  73. data/app/models/refine/stabilizers/database_stabilizer.rb +21 -0
  74. data/app/models/refine/stabilizers/errors/url_stabilizer_error.rb +2 -0
  75. data/app/models/refine/stabilizers/url_encoded_stabilizer.rb +21 -0
  76. data/app/models/refine/stored_filter.rb +14 -0
  77. data/app/models/refine/tracks_pending_relationship_subqueries.rb +196 -0
  78. data/app/views/_filter_builder_dropdown.html.erb +63 -0
  79. data/app/views/_filter_pills.html.erb +40 -0
  80. data/app/views/_loading.html.erb +32 -0
  81. data/app/views/refine/blueprints/_add_and.html.erb +25 -0
  82. data/app/views/refine/blueprints/_add_group.html.erb +24 -0
  83. data/app/views/refine/blueprints/_clause_select.html.erb +24 -0
  84. data/app/views/refine/blueprints/_condition_select.html.erb +53 -0
  85. data/app/views/refine/blueprints/_criterion.html.erb +41 -0
  86. data/app/views/refine/blueprints/_criterion_errors.html.erb +7 -0
  87. data/app/views/refine/blueprints/_delete_criterion.html.erb +11 -0
  88. data/app/views/refine/blueprints/_group.html.erb +13 -0
  89. data/app/views/refine/blueprints/_query.html.erb +34 -0
  90. data/app/views/refine/blueprints/_stored_filters.html.erb +23 -0
  91. data/app/views/refine/blueprints/clauses/_date_condition.html.erb +80 -0
  92. data/app/views/refine/blueprints/clauses/_date_picker.html.erb +26 -0
  93. data/app/views/refine/blueprints/clauses/_filter_condition.html.erb +36 -0
  94. data/app/views/refine/blueprints/clauses/_numeric_condition.html.erb +35 -0
  95. data/app/views/refine/blueprints/clauses/_option_condition.html.erb +37 -0
  96. data/app/views/refine/blueprints/clauses/_text_condition.html.erb +13 -0
  97. data/app/views/refine/blueprints/create.turbo_stream.erb +22 -0
  98. data/app/views/refine/blueprints/new.html.erb +7 -0
  99. data/app/views/refine/blueprints/show.html.erb +4 -0
  100. data/app/views/refine/blueprints/show.turbo_stream.erb +22 -0
  101. data/app/views/refine/inline/criteria/_form_fields.html.erb +62 -0
  102. data/app/views/refine/inline/criteria/create.turbo_stream.erb +19 -0
  103. data/app/views/refine/inline/criteria/edit.turbo_stream.erb +26 -0
  104. data/app/views/refine/inline/criteria/index.html.erb +64 -0
  105. data/app/views/refine/inline/criteria/new.turbo_stream.erb +24 -0
  106. data/app/views/refine/inline/filters/_add_first_condition_button.html.erb +19 -0
  107. data/app/views/refine/inline/filters/_and_button.html.erb +26 -0
  108. data/app/views/refine/inline/filters/_criterion.html.erb +23 -0
  109. data/app/views/refine/inline/filters/_group.html.erb +13 -0
  110. data/app/views/refine/inline/filters/_load_button.html.erb +15 -0
  111. data/app/views/refine/inline/filters/_or_button.html.erb +26 -0
  112. data/app/views/refine/inline/filters/_popup.html.erb +26 -0
  113. data/app/views/refine/inline/filters/_save_button.html.erb +15 -0
  114. data/app/views/refine/inline/filters/_show.html.erb +40 -0
  115. data/app/views/refine/inline/inputs/_date_condition.html.erb +7 -0
  116. data/app/views/refine/inline/inputs/_date_condition_days.html.erb +18 -0
  117. data/app/views/refine/inline/inputs/_date_condition_range.html.erb +22 -0
  118. data/app/views/refine/inline/inputs/_date_condition_single.html.erb +9 -0
  119. data/app/views/refine/inline/inputs/_date_picker.html.erb +20 -0
  120. data/app/views/refine/inline/inputs/_numeric_condition.html.erb +23 -0
  121. data/app/views/refine/inline/inputs/_option_condition.html.erb +14 -0
  122. data/app/views/refine/inline/inputs/_text_condition.html.erb +8 -0
  123. data/app/views/refine/inline/stored_filters/find.turbo_stream.erb +19 -0
  124. data/app/views/refine/inline/stored_filters/index.html.erb +28 -0
  125. data/app/views/refine/inline/stored_filters/new.turbo_stream.erb +47 -0
  126. data/app/views/refine/stored_filters/create.turbo_stream.erb +2 -0
  127. data/app/views/refine/stored_filters/find.turbo_stream.erb +5 -0
  128. data/app/views/refine/stored_filters/index.html.erb +39 -0
  129. data/app/views/refine/stored_filters/new.html.erb +29 -0
  130. data/app/views/refine/stored_filters/show.html.erb +1 -0
  131. data/config/locales/en/dates.en.yml +29 -0
  132. data/config/locales/en/en.yml +20 -0
  133. data/config/locales/en/refine.en.yml +187 -0
  134. data/config/routes.rb +17 -0
  135. data/lib/generators/filter/filter_generator.rb +27 -0
  136. data/lib/generators/filter/templates/filter.rb.erb +20 -0
  137. data/lib/refine/rails/engine.rb +15 -0
  138. data/lib/refine/rails/version.rb +5 -0
  139. data/lib/refine/rails.rb +38 -0
  140. data/lib/tasks/refine/rails_tasks.rake +13 -0
  141. 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,13 @@
1
+ class Refine::Inline::Criteria::NumericRefinement
2
+ include ActiveModel::Model
3
+
4
+ attr_accessor :clause, :value1, :value2
5
+
6
+ def attributes
7
+ {
8
+ clause: clause,
9
+ value1: value1,
10
+ value2: value2
11
+ }.compact
12
+ end
13
+ end
@@ -0,0 +1,2 @@
1
+ class Refine::Inline::Criteria::Option < Struct.new(:id, :display, keyword_init: true)
2
+ 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,8 @@
1
+ class Refine::InvalidFilterError < StandardError
2
+ attr_reader :filter
3
+
4
+ def initialize(msg="Filter is invalid", filter: nil)
5
+ @msg = msg
6
+ filter = filter
7
+ end
8
+ 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,2 @@
1
+ class Refine::Stabilizers::Errors::UrlStabilizerError < StandardError
2
+ 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