refine-rails 2.9.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.
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