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,10 @@
1
+ module Refine::Conditions::HasMeta
2
+ def meta
3
+ @meta ||= {}
4
+ end
5
+
6
+ def with_meta(value)
7
+ meta.merge!(value)
8
+ self
9
+ end
10
+ end
@@ -0,0 +1,156 @@
1
+ module Refine::Conditions
2
+ module HasRefinements
3
+ def refine_by_filter(filter_condition)
4
+ @filter_refinement_proc = proc { filter_condition }
5
+ self
6
+ end
7
+
8
+ def refine_by_date(callable = nil)
9
+ # Developer can send in a string that represents the attribute or a proc that is a fully qualified class
10
+ # If a callable is given and it is a string, we assume that is the desired attribute. Otherwise we use
11
+ # the passed in callable. If no callable is given we create on based on standard date with time condition
12
+ @date_refinement_proc = if callable.present?
13
+ (callable.is_a? String) ? define_date_condition(attribute: callable) : callable
14
+ else
15
+ define_date_condition(attribute: "created_at")
16
+ end
17
+ self
18
+ end
19
+
20
+ def define_date_condition(attribute:)
21
+ proc { DateCondition.new("date_refinement").with_attribute(attribute).attribute_is_date_with_time }
22
+ end
23
+
24
+ def refine_by_count(callable = nil)
25
+ @count_refinement_proc = callable.present? ? callable : define_count_condition
26
+ self
27
+ end
28
+
29
+ def define_count_condition
30
+ proc { NumericCondition.new("count_refinement") }
31
+ end
32
+
33
+ def apply_refinements(input)
34
+ if has_any_refinements? && !refinements_allowed?
35
+ raise Errors::RelationshipError, I18n.t("refine.refine_blueprints.has_refinements.not_allowed")
36
+ end
37
+
38
+ instance = filter.get_pending_relationship_instance
39
+
40
+ subquery_table = instance.klass.arel_table
41
+ subquery = filter.get_pending_relationship_subquery
42
+
43
+ if @date_refinement_proc
44
+ nodes = apply_date_refinement(subquery_table, input[:date_refinement])
45
+ end
46
+ if @count_refinement_proc
47
+ apply_count_refinement(table: subquery_table, input: input[:count_refinement], subquery: subquery, instance: instance)
48
+ end
49
+ nodes
50
+ end
51
+
52
+ def has_any_refinements?
53
+ !!(has_date_refinement? || has_count_refinement?)
54
+ end
55
+
56
+ def has_count_refinement?
57
+ @count_refinement_proc
58
+ end
59
+
60
+ def has_date_refinement?
61
+ @date_refinement_proc
62
+ end
63
+
64
+ def refinements_allowed?
65
+ instance = filter.get_pending_relationship_instance
66
+ (instance.is_a? ActiveRecord::Reflection::HasManyReflection) || (instance.is_a? ActiveRecord::Reflection::ThroughReflection)
67
+ end
68
+
69
+ def apply_date_refinement(table, input)
70
+ get_date_refinement_condition.apply(input, table, nil)
71
+ end
72
+
73
+ def apply_count_refinement(table:, input:, subquery:, instance:)
74
+ condition = get_count_refinement_condition
75
+ # We need to group by because we're going to be using a `having`
76
+ # to get the count of records. Since we're using a where in for this
77
+ # relationship subquery, we're only selecting the foreign key,
78
+ # so we'll just group by that too.
79
+ subquery.group(table[instance.foreign_key])
80
+
81
+ # If user's input doesn't include the number 0, there is no complicated
82
+ # joining
83
+ if condition.input_could_include_zero?(input)
84
+ # if the input DOES include 0 then we have to do a bit of extra work. If you were trying to get a list of
85
+ # contacts that has i.e. 0 events, you can't query the events table b/c those contacts
86
+ # don't have any record there.
87
+ # Get a list of all contacts, left join in the count of events we're looking for, and coalesce nulls
88
+ # to 0. That gives us a true count for every contact, even if the count is 0. Then we can use
89
+ # the Numeric Condition as usual.
90
+ # Wrap the existing subquery - subquery is an AREL Select manager and modified in place
91
+ subquery.project((Arel.star.count).as("hs_refine_count_aggregate"))
92
+
93
+ # The table that owns the relationship
94
+ # TODO This is redundant
95
+ parent_table = instance.active_record.arel_table
96
+ parent_primary_key = instance.active_record.primary_key
97
+
98
+ # SELECT "contacts"."id" FROM "contacts"
99
+ outer_query = parent_table.project(parent_table[parent_primary_key.to_sym])
100
+
101
+ callable_subquery =
102
+ proc do |inner_query, primary_key, foreign_key|
103
+ interim_table = inner_query.as("interim_table")
104
+ outer_query.join(interim_table, Arel::Nodes::OuterJoin).on(interim_table[foreign_key.to_sym].eq(parent_table[primary_key.to_sym]))
105
+ end
106
+
107
+ filter.set_pending_relationship_subquery_wrapper(callable_subquery)
108
+
109
+ condition.raw_attribute("coalesce(hs_refine_count_aggregate, 0)")
110
+
111
+ node = condition.apply(input, table, nil)
112
+ outer_query.where(node)
113
+ else
114
+ node = condition.apply(input, table, nil)
115
+ # Modify the pending relationship subquery in place to be applied later
116
+ # Remember, AREL in this instance has passed by reference so pending_relationship_subquery will be modified
117
+ subquery.having(node)
118
+ # Return nil case due to pending relationship subquery not yet being applied
119
+ nil
120
+ end
121
+ end
122
+
123
+ def get_date_refinement_condition
124
+ # Create the condition from the callable
125
+ condition = @date_refinement_proc.call
126
+ # Overwrite any passed in id's with date_refinement
127
+ condition.id = "date_refinement"
128
+ condition.is_refinement = true
129
+ filter.instantiate_condition(condition)
130
+ end
131
+
132
+ def get_count_refinement_condition
133
+ condition = @count_refinement_proc.call
134
+ # Overwrite any passed in id's with count_refinement
135
+ condition.id = "count_refinement"
136
+ condition.is_refinement = true
137
+ condition.raw_attribute("COUNT(*)")
138
+ filter.instantiate_condition(condition)
139
+ end
140
+
141
+ def refinements_to_array
142
+ if is_refinement
143
+ []
144
+ else
145
+ refinement_array = []
146
+ if @date_refinement_proc
147
+ refinement_array << get_date_refinement_condition.to_array
148
+ end
149
+ if @count_refinement_proc
150
+ refinement_array << get_count_refinement_condition.to_array
151
+ end
152
+ refinement_array
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,224 @@
1
+ module Refine::Conditions
2
+ class NumericCondition < Condition
3
+ include ActiveModel::Validations
4
+ include HasClauses
5
+
6
+ cattr_accessor :default_clause_display_map, default: {}, instance_accessor: false
7
+
8
+ validates :value1, numericality: true, allow_nil: true
9
+ validates :value2, numericality: true, allow_nil: true
10
+
11
+ with_options if: :floats_not_allowed? do
12
+ validates :value1, numericality: {only_integer: true}, allow_nil: true
13
+ validates :value2, numericality: {only_integer: true}, allow_nil: true
14
+ end
15
+
16
+ attr_reader :value1, :value2
17
+
18
+ CLAUSE_EQUALS = Clauses::EQUALS
19
+ CLAUSE_DOESNT_EQUAL = Clauses::DOESNT_EQUAL
20
+
21
+ CLAUSE_LESS_THAN_OR_EQUAL = Clauses::LESS_THAN_OR_EQUAL
22
+ CLAUSE_LESS_THAN = Clauses::LESS_THAN
23
+ CLAUSE_GREATER_THAN = Clauses::GREATER_THAN
24
+ CLAUSE_GREATER_THAN_OR_EQUAL = Clauses::GREATER_THAN_OR_EQUAL
25
+
26
+ CLAUSE_BETWEEN = Clauses::BETWEEN
27
+ CLAUSE_NOT_BETWEEN = Clauses::NOT_BETWEEN
28
+
29
+ CLAUSE_SET = Clauses::SET
30
+ CLAUSE_NOT_SET = Clauses::NOT_SET
31
+
32
+ I18N_PREFIX = "refine.refine_blueprints.numeric_condition."
33
+
34
+ def boot
35
+ @floats = false
36
+ end
37
+
38
+ def set_input_parameters(input)
39
+ @value1 = input[:value1]
40
+ @value2 = input[:value2]
41
+ end
42
+
43
+ def component
44
+ "numeric-condition"
45
+ end
46
+
47
+ def human_readable(input)
48
+ current_clause = get_clause_by_id(input[:clause])
49
+ case input[:clause]
50
+ when *[CLAUSE_EQUALS, CLAUSE_DOESNT_EQUAL, CLAUSE_GREATER_THAN, CLAUSE_GREATER_THAN_OR_EQUAL, CLAUSE_LESS_THAN, CLAUSE_LESS_THAN_OR_EQUAL]
51
+ "#{display} #{current_clause.display} #{input[:value1]}"
52
+ when *[CLAUSE_BETWEEN, CLAUSE_NOT_BETWEEN]
53
+ "#{display} #{current_clause.display} #{input[:value1]} #{I18n.t("#{I18N_PREFIX}and")} #{input[:value2]}"
54
+ when *[CLAUSE_SET, CLAUSE_NOT_SET]
55
+ "#{display} #{current_clause.display}"
56
+ else
57
+ raise "#{input[:clause]} #{I18n.t("#{I18N_PREFIX}not_supported")}"
58
+ end
59
+ end
60
+
61
+ def human_readable_value(input)
62
+ current_clause = get_clause_by_id(input[:clause])
63
+ case input[:clause]
64
+ when *[CLAUSE_EQUALS, CLAUSE_DOESNT_EQUAL, CLAUSE_GREATER_THAN, CLAUSE_GREATER_THAN_OR_EQUAL, CLAUSE_LESS_THAN, CLAUSE_LESS_THAN_OR_EQUAL]
65
+ input[:value1]
66
+ when *[CLAUSE_BETWEEN, CLAUSE_NOT_BETWEEN]
67
+ "#{input[:value1]} #{I18n.t("#{I18N_PREFIX}and")} #{input[:value2]}"
68
+ when *[CLAUSE_SET, CLAUSE_NOT_SET]
69
+ ""
70
+ else
71
+ raise "#{input[:clause]} #{I18n.t("#{I18N_PREFIX}not_supported")}"
72
+ end
73
+ end
74
+
75
+
76
+
77
+ def clauses
78
+ [
79
+ Clause.new(CLAUSE_EQUALS, I18n.t("#{I18N_PREFIX}is")).requires_inputs(["value1"]),
80
+
81
+ Clause.new(CLAUSE_DOESNT_EQUAL, I18n.t("#{I18N_PREFIX}is_not")).requires_inputs(["value1"]),
82
+
83
+ Clause.new(CLAUSE_GREATER_THAN, I18n.t("#{I18N_PREFIX}is_gt")).requires_inputs(["value1"]),
84
+
85
+ Clause.new(CLAUSE_GREATER_THAN_OR_EQUAL, I18n.t("#{I18N_PREFIX}is_gtteq")).requires_inputs(["value1"]),
86
+
87
+ Clause.new(CLAUSE_LESS_THAN, I18n.t("#{I18N_PREFIX}is_lt")).requires_inputs(["value1"]),
88
+
89
+ Clause.new(CLAUSE_LESS_THAN_OR_EQUAL, I18n.t("#{I18N_PREFIX}is_lteq")).requires_inputs(["value1"]),
90
+
91
+ Clause.new(CLAUSE_BETWEEN, I18n.t("#{I18N_PREFIX}is_between")).requires_inputs(["value1", "value2"]),
92
+
93
+ Clause.new(CLAUSE_NOT_BETWEEN, I18n.t("#{I18N_PREFIX}is_not_between")).requires_inputs(["value1", "value2"]),
94
+
95
+ Clause.new(CLAUSE_SET, I18n.t("#{I18N_PREFIX}is_set")),
96
+
97
+ Clause.new(CLAUSE_NOT_SET, I18n.t("#{I18N_PREFIX}is_not_set")),
98
+ ]
99
+ end
100
+
101
+ def allow_floats
102
+ @floats = true
103
+ self
104
+ end
105
+
106
+ def floats_not_allowed?
107
+ !@floats
108
+ end
109
+
110
+ # TODO Refactor to remove input here
111
+ def apply_condition(input, table, _inverse_clause)
112
+ # TODO check for custom clause
113
+
114
+ case clause
115
+ when CLAUSE_EQUALS
116
+ apply_clause_equals(table, value1)
117
+
118
+ when CLAUSE_DOESNT_EQUAL
119
+ apply_clause_doesnt_equal(table, value1)
120
+
121
+ when CLAUSE_GREATER_THAN
122
+ apply_clause_greater_than(table, value1)
123
+
124
+ when CLAUSE_GREATER_THAN_OR_EQUAL
125
+ apply_clause_greater_than_or_equal(table, value1)
126
+
127
+ when CLAUSE_LESS_THAN
128
+ apply_clause_less_than(table, value1)
129
+
130
+ when CLAUSE_LESS_THAN_OR_EQUAL
131
+ apply_clause_less_than_or_equal(table, value1)
132
+
133
+ when CLAUSE_BETWEEN
134
+ apply_clause_between(table, value1, value2)
135
+
136
+ when CLAUSE_NOT_BETWEEN
137
+ apply_clause_not_between(table, value1, value2)
138
+
139
+ when CLAUSE_SET
140
+ apply_clause_set(table)
141
+
142
+ when CLAUSE_NOT_SET
143
+ apply_clause_not_set(table)
144
+ end
145
+ end
146
+
147
+ def input_could_include_zero?(input)
148
+ clause = input[:clause]
149
+ value1 = input[:value1].to_i
150
+ value2 = input[:value2].to_i
151
+ case clause
152
+ when CLAUSE_EQUALS
153
+ return value1 == 0
154
+
155
+ when CLAUSE_DOESNT_EQUAL
156
+ return value1 != 0
157
+
158
+ when CLAUSE_LESS_THAN_OR_EQUAL
159
+ return value1 >= 0
160
+
161
+ when CLAUSE_LESS_THAN
162
+ return value1 > 0
163
+
164
+ when CLAUSE_GREATER_THAN
165
+ return value1 < 0
166
+
167
+ when CLAUSE_GREATER_THAN_OR_EQUAL
168
+ return value1 <= 0
169
+
170
+ when CLAUSE_BETWEEN
171
+ return value1 <= 0 && value2 >= 0
172
+
173
+ when CLAUSE_NOT_BETWEEN
174
+ return (value1 > 0 && value2 > 0) || (value1 < 0 && value2 < 0)
175
+
176
+ when CLAUSE_SET
177
+ return false
178
+
179
+ when CLAUSE_NOT_SET
180
+ return false
181
+ end
182
+ end
183
+
184
+ def apply_clause_equals(table, value)
185
+ table.grouping(arel_attribute(table).eq(value))
186
+ end
187
+
188
+ def apply_clause_doesnt_equal(table, value)
189
+ table.grouping(arel_attribute(table).not_eq(value).or(arel_attribute(table).eq(nil)))
190
+ end
191
+
192
+ def apply_clause_greater_than(table, value)
193
+ table.grouping(arel_attribute(table).gt(value))
194
+ end
195
+
196
+ def apply_clause_greater_than_or_equal(table, value)
197
+ table.grouping(arel_attribute(table).gteq(value))
198
+ end
199
+
200
+ def apply_clause_less_than(table, value)
201
+ table.grouping(arel_attribute(table).lt(value))
202
+ end
203
+
204
+ def apply_clause_less_than_or_equal(table, value)
205
+ table.grouping(arel_attribute(table).lteq(value))
206
+ end
207
+
208
+ def apply_clause_between(table, value1, value2)
209
+ table.grouping(arel_attribute(table).between(value1..value2))
210
+ end
211
+
212
+ def apply_clause_not_between(table, value1, value2)
213
+ table.grouping(arel_attribute(table).not_between(value1..value2))
214
+ end
215
+
216
+ def apply_clause_set(table)
217
+ table.grouping(arel_attribute(table).not_eq(nil))
218
+ end
219
+
220
+ def apply_clause_not_set(table)
221
+ table.grouping(arel_attribute(table).eq(nil))
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,260 @@
1
+ module Refine::Conditions
2
+ class OptionCondition < Condition
3
+ include HasClauses
4
+ include UsesAttributes
5
+ include ActiveModel::Validations
6
+
7
+ validate :select_is_array
8
+ validate :option_in_approved_list?
9
+
10
+ attr_reader :selected, :nil_option_id, :options
11
+
12
+ CLAUSE_EQUALS = Clauses::EQUALS
13
+ CLAUSE_DOESNT_EQUAL = Clauses::DOESNT_EQUAL
14
+
15
+ CLAUSE_IN = Clauses::IN
16
+ CLAUSE_NOT_IN = Clauses::NOT_IN
17
+
18
+ CLAUSE_SET = Clauses::SET
19
+ CLAUSE_NOT_SET = Clauses::NOT_SET
20
+
21
+ I18N_PREFIX = "refine.refine_blueprints.option_condition."
22
+
23
+ def component
24
+ "option-condition"
25
+ end
26
+
27
+ def human_readable(input)
28
+ current_clause = get_clause_by_id(input[:clause])
29
+ display_values = input[:selected]&.map {|option_id| get_options.call.find{|option| option[:id] == option_id}[:display]}.to_a
30
+ case input[:clause]
31
+ when *[CLAUSE_EQUALS, CLAUSE_DOESNT_EQUAL]
32
+ "#{display} #{current_clause.display} #{display_values.first}"
33
+ when *[CLAUSE_IN, CLAUSE_NOT_IN]
34
+ if display_values.length >= 3
35
+ display_values = display_values.take(2) + ["..."]
36
+ end
37
+ "#{display} #{current_clause.display}: #{display_values.join(", ")}"
38
+ when *[CLAUSE_SET, CLAUSE_NOT_SET]
39
+ "#{display} #{current_clause.display}"
40
+ else
41
+ raise "#{input[:clause]} #{I18n.t("#{I18N_PREFIX}not_supported")}"
42
+ end
43
+ end
44
+
45
+ def human_readable_value(input)
46
+ current_clause = get_clause_by_id(input[:clause])
47
+ display_values = input[:selected]&.map {|option_id| get_options.call.find{|option| option[:id] == option_id}[:display]}.to_a
48
+ case input[:clause]
49
+ when *[CLAUSE_EQUALS, CLAUSE_DOESNT_EQUAL]
50
+ display_values.first
51
+ when *[CLAUSE_IN, CLAUSE_NOT_IN]
52
+ if display_values.length >= 3
53
+ display_values = display_values.take(2) + ["..."]
54
+ end
55
+ display_values.join(", ")
56
+ when *[CLAUSE_SET, CLAUSE_NOT_SET]
57
+ ""
58
+ else
59
+ raise "#{input[:clause]} #{I18n.t("#{I18N_PREFIX}not_supported")}"
60
+ end
61
+ end
62
+
63
+ def boot
64
+ @nil_option_id = nil
65
+ @options = nil
66
+ # TODO @validate_selections = true
67
+ with_meta({options: get_options})
68
+ add_ensurance(ensure_options)
69
+ end
70
+
71
+ def set_input_parameters(input)
72
+ @selected = input[:selected]
73
+ end
74
+
75
+ def select_is_array
76
+ errors.add(:base, I18n.t("#{I18N_PREFIX}must_be_array")) unless selected.is_a?(Array)
77
+ end
78
+
79
+ def option_in_approved_list?
80
+ # TODO allow this to accept integers as well as strings. Right now must be a string.
81
+ return if selected.nil?
82
+ selected.each do |select|
83
+ select.join if select.is_a? Array
84
+ unless get_options.call.map { |option| option[:id] }.include? select
85
+ errors.add(:base, I18n.t("#{I18N_PREFIX}not_approved", select: select))
86
+ end
87
+ end
88
+ end
89
+
90
+ def get_options
91
+ proc do
92
+ @options = Refine::Rails.configuration.option_condition_ordering.call(
93
+ call_proc_if_callable(options)
94
+ )
95
+ end
96
+ end
97
+
98
+ def ensure_options
99
+ proc do
100
+ developer_options = get_options.call
101
+ # Options must be sent in as an array
102
+ if !developer_options.is_a? Array
103
+ raise I18n.t("#{I18N_PREFIX}not_determined")
104
+ end
105
+ # Each option must be a hash of values that includes :id and :display
106
+ developer_options.each do |option|
107
+ if (!option.is_a? Hash) || option.keys.exclude?(:id) || option.keys.exclude?(:display)
108
+ raise Refine::Conditions::Errors::OptionError.new(I18n.t("#{I18N_PREFIX}must_have_id_and_display"))
109
+ end
110
+ end
111
+ ensure_no_duplicates(developer_options)
112
+ end
113
+ end
114
+
115
+ def ensure_no_duplicates(developer_options)
116
+ id_array = developer_options.map { |option| option[:id] }
117
+ duplicates = id_array.select { |id| id_array.count(id) > 1 }.uniq
118
+ if duplicates.present?
119
+ raise Refine::Conditions::Errors::OptionError.new(I18n.t("#{I18N_PREFIX}must_be_unique", duplicates: duplicates))
120
+ end
121
+ end
122
+
123
+ def clauses
124
+ [
125
+ Clause.new(CLAUSE_EQUALS, I18n.t("#{I18N_PREFIX}is"))
126
+ .requires_inputs(["selected"])
127
+ .with_meta({multiple: false}),
128
+
129
+ Clause.new(CLAUSE_DOESNT_EQUAL, I18n.t("#{I18N_PREFIX}is_not"))
130
+ .requires_inputs(["selected"])
131
+ .with_meta({multiple: false}),
132
+
133
+ Clause.new(CLAUSE_IN, I18n.t("#{I18N_PREFIX}is_one_of"))
134
+ .requires_inputs(["selected"])
135
+ .with_meta({multiple: true}),
136
+
137
+ Clause.new(CLAUSE_NOT_IN, I18n.t("#{I18N_PREFIX}is_not_one_of"))
138
+ .requires_inputs(["selected"])
139
+ .with_meta({multiple: true}),
140
+
141
+ Clause.new(CLAUSE_SET, I18n.t("#{I18N_PREFIX}is_set")),
142
+
143
+ Clause.new(CLAUSE_NOT_SET, I18n.t("#{I18N_PREFIX}is_not_set"))
144
+ ]
145
+ end
146
+
147
+ def apply_condition(input, table, inverse_clause)
148
+ value = input[:selected]
149
+ # TODO: Triggers on "through" relationship. Other relationships?
150
+ @clause = CLAUSE_IN if inverse_clause
151
+
152
+ case clause
153
+ when CLAUSE_SET
154
+ apply_clause_set(table)
155
+
156
+ when CLAUSE_NOT_SET
157
+ apply_clause_not_set(table)
158
+
159
+ when CLAUSE_EQUALS
160
+ apply_clause_equals(value, table)
161
+
162
+ when CLAUSE_DOESNT_EQUAL
163
+ apply_clause_doesnt_equal(value, table)
164
+
165
+ when CLAUSE_IN
166
+ apply_clause_in(value, table)
167
+
168
+ when CLAUSE_NOT_IN
169
+ apply_clause_not_in(value, table)
170
+ end
171
+ end
172
+
173
+ def with_options(developer_configured_options)
174
+ @options = developer_configured_options
175
+ self
176
+ end
177
+
178
+ def with_nil_option(id)
179
+ @nil_option_id = id
180
+ self
181
+ end
182
+
183
+ def nil_option_selected?(value)
184
+ # Return false if no nil option id
185
+ return false unless nil_option_id
186
+ value&.include? nil_option_id
187
+ end
188
+
189
+ def values_for_application(ids, single = false)
190
+ # Get developer configured options with nil_option_id removed and select only elements from requested ids
191
+ # Extract values from either _value key or id key. _value can be a callable
192
+ values = get_options.call.delete_if { |el| el[:id] == nil_option_id }
193
+ .select { |value| ids.include? value[:id] }
194
+ .map! { |value| (value.has_key? :_value) ? call_proc_if_callable(value[:_value]) : value[:id] }
195
+ single ? values[0] : values
196
+ end
197
+
198
+ def apply_nil_query(value, table)
199
+ table.grouping(table[:"#{attribute}"].eq(nil))
200
+ end
201
+
202
+ def apply_not_nil_query(value, table)
203
+ table.grouping(table[:"#{attribute}"].not_eq(nil))
204
+ end
205
+
206
+ def apply_equals(value, table)
207
+ table.grouping(table[:"#{attribute}"].eq(value))
208
+ end
209
+
210
+ def apply_clause_in(value, table)
211
+ normalized_values = values_for_application(value)
212
+
213
+ if nil_option_selected?(value)
214
+ table.grouping(table[:"#{attribute}"].in(normalized_values).or(table[:"#{attribute}"].eq(nil)))
215
+ else
216
+ table.grouping(table[:"#{attribute}"].in(normalized_values))
217
+ end
218
+ end
219
+
220
+ def apply_clause_not_in(value, table)
221
+ normalized_values = values_for_application(value)
222
+ # Must check for only nil option selected here
223
+ if nil_option_selected?(value) && value.one?
224
+ table.grouping(table[:"#{attribute}"].not_eq(nil))
225
+ elsif nil_option_selected?(value)
226
+ table.grouping(table[:"#{attribute}"].not_in(normalized_values).or(table[:"#{attribute}"].not_eq(nil)))
227
+ else
228
+ table.grouping(table[:"#{attribute}"].not_in(normalized_values).or(table[:"#{attribute}"].eq(nil)))
229
+ end
230
+ end
231
+
232
+ def apply_clause_equals(value, table)
233
+ if nil_option_selected?(value)
234
+ apply_nil_query(value, table)
235
+ else
236
+ apply_equals(values_for_application(value, true), table)
237
+ end
238
+ end
239
+
240
+ def apply_clause_doesnt_equal(value, table)
241
+ if nil_option_selected?(value)
242
+ apply_not_nil_query(value, table)
243
+ else
244
+ apply_not_equals(values_for_application(value, true), table)
245
+ end
246
+ end
247
+
248
+ def apply_not_equals(value, table)
249
+ table.grouping(table[:"#{attribute}"].not_eq(value).or(table[:"#{attribute}"].eq(nil)))
250
+ end
251
+
252
+ def apply_clause_set(table)
253
+ table.grouping(table[:"#{attribute}"].not_eq_any([nil, ""]))
254
+ end
255
+
256
+ def apply_clause_not_set(table)
257
+ table.grouping(table[:"#{attribute}"].eq_any([nil, ""]))
258
+ end
259
+ end
260
+ end