headmin 0.4.1 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.lock-487e157d270f3062a98b7b2a012753708-1272821827 +0 -0
- data/CHANGELOG.md +16 -2
- data/Gemfile.lock +77 -79
- data/app/assets/javascripts/headmin/controllers/date_range_controller.js +12 -6
- data/app/assets/javascripts/headmin/controllers/filter_controller.js +61 -11
- data/app/assets/javascripts/headmin/controllers/filter_row_controller.js +50 -0
- data/app/assets/javascripts/headmin/controllers/flatpickr_controller.js +2 -6
- data/app/assets/javascripts/headmin/controllers/popup_controller.js +3 -1
- data/app/assets/javascripts/headmin/controllers/table_actions_controller.js +16 -21
- data/app/assets/javascripts/headmin/index.js +2 -0
- data/app/assets/javascripts/headmin.js +119 -38
- data/app/assets/stylesheets/headmin/filter.scss +74 -0
- data/app/assets/stylesheets/headmin/general.scss +0 -1
- data/app/assets/stylesheets/headmin/layout/body.scss +5 -0
- data/app/assets/stylesheets/headmin/popup.scss +0 -1
- data/app/assets/stylesheets/headmin/table.scss +7 -0
- data/app/assets/stylesheets/headmin.css +73 -2
- data/app/controllers/concerns/headmin/filterable.rb +27 -0
- data/app/models/concerns/headmin/field.rb +4 -2
- data/app/models/concerns/headmin/fieldable.rb +138 -44
- data/app/models/headmin/blocks_view.rb +1 -1
- data/app/models/headmin/filter/base.rb +238 -0
- data/app/models/headmin/filter/base_view.rb +64 -0
- data/app/models/headmin/filter/boolean.rb +15 -0
- data/app/models/headmin/filter/boolean_view.rb +61 -0
- data/app/models/headmin/filter/button_view.rb +25 -0
- data/app/models/headmin/filter/conditional_view.rb +16 -0
- data/app/models/headmin/filter/date.rb +19 -0
- data/app/models/headmin/filter/date_view.rb +52 -0
- data/app/models/headmin/filter/flatpickr_view.rb +54 -0
- data/app/models/headmin/filter/menu_item_view.rb +6 -0
- data/app/models/headmin/filter/money.rb +13 -0
- data/app/models/headmin/filter/number.rb +27 -0
- data/app/models/headmin/filter/number_view.rb +54 -0
- data/app/models/headmin/filter/operator_view.rb +30 -0
- data/app/models/headmin/filter/options_view.rb +61 -0
- data/app/models/headmin/filter/row_view.rb +13 -0
- data/app/models/headmin/filter/search.rb +18 -0
- data/app/models/headmin/filter/search_view.rb +31 -0
- data/app/models/headmin/filter/text.rb +25 -0
- data/app/models/headmin/filter/text_view.rb +53 -0
- data/app/models/headmin/filters.rb +29 -0
- data/app/models/headmin/form/color_view.rb +48 -0
- data/app/models/headmin/form/datetime_range_view.rb +25 -0
- data/app/models/headmin/form/datetime_view.rb +45 -0
- data/app/models/headmin/form/flatpickr_range_view.rb +4 -15
- data/app/models/headmin/form/flatpickr_view.rb +3 -12
- data/app/models/view_model.rb +1 -1
- data/app/views/examples/admin.html.erb +13 -13
- data/app/views/examples/auth.html.erb +1 -1
- data/app/views/headmin/_blocks.html.erb +1 -1
- data/app/views/headmin/_filters.html.erb +6 -6
- data/app/views/headmin/_form.html.erb +2 -2
- data/app/views/headmin/_index.html.erb +1 -1
- data/app/views/headmin/_pagination.html.erb +1 -1
- data/app/views/headmin/_popup.html.erb +2 -2
- data/app/views/headmin/_table.html.erb +1 -1
- data/app/views/headmin/dropdown/_devise.html.erb +8 -8
- data/app/views/headmin/dropdown/_locale.html.erb +4 -4
- data/app/views/headmin/filters/_base.html.erb +95 -0
- data/app/views/headmin/filters/_boolean.html.erb +23 -0
- data/app/views/headmin/filters/_date.html.erb +14 -38
- data/app/views/headmin/filters/_flatpickr.html.erb +15 -48
- data/app/views/headmin/filters/_number.html.erb +23 -0
- data/app/views/headmin/filters/_options.html.erb +24 -0
- data/app/views/headmin/filters/_search.html.erb +14 -12
- data/app/views/headmin/filters/_text.html.erb +23 -0
- data/app/views/headmin/filters/filter/_button.html.erb +9 -10
- data/app/views/headmin/filters/filter/_conditional.html.erb +18 -0
- data/app/views/headmin/filters/filter/_menu_item.html.erb +5 -2
- data/app/views/headmin/filters/filter/_null_select.html.erb +8 -0
- data/app/views/headmin/filters/filter/_operator.html.erb +16 -0
- data/app/views/headmin/filters/filter/_row.html.erb +11 -0
- data/app/views/headmin/forms/_blocks.html.erb +1 -1
- data/app/views/headmin/forms/_color.html.erb +32 -0
- data/app/views/headmin/forms/_date_range.html.erb +3 -3
- data/app/views/headmin/forms/_datetime.html.erb +41 -0
- data/app/views/headmin/forms/_datetime_range.html.erb +40 -0
- data/app/views/headmin/forms/_file.html.erb +3 -3
- data/app/views/headmin/forms/_flatpickr.html.erb +1 -1
- data/app/views/headmin/forms/_flatpickr_range.html.erb +3 -4
- data/app/views/headmin/forms/_label.html.erb +1 -1
- data/app/views/headmin/forms/_repeater.html.erb +12 -12
- data/app/views/headmin/forms/fields/_base.html.erb +1 -1
- data/app/views/headmin/forms/fields/_file.html.erb +3 -3
- data/app/views/headmin/forms/fields/_files.html.erb +17 -0
- data/app/views/headmin/forms/fields/_group.html.erb +5 -5
- data/app/views/headmin/forms/fields/_list.html.erb +4 -4
- data/app/views/headmin/forms/fields/_text.html.erb +2 -2
- data/app/views/headmin/layout/_footer.html.erb +1 -1
- data/app/views/headmin/layout/_main.html.erb +1 -1
- data/app/views/headmin/nav/_dropdown.html.erb +3 -3
- data/app/views/headmin/nav/_item.html.erb +2 -2
- data/app/views/headmin/nav/item/_devise.html.erb +8 -8
- data/app/views/headmin/nav/item/_locale.html.erb +4 -4
- data/app/views/headmin/table/_actions.html.erb +3 -6
- data/app/views/headmin/table/actions/_export.html.erb +1 -1
- data/app/views/headmin/table/body/_color.html.erb +10 -0
- data/app/views/headmin/table/body/_row.html.erb +3 -3
- data/app/views/headmin/table/foot/_id.html.erb +1 -1
- data/app/views/headmin/views/devise/confirmations/_new.html.erb +1 -1
- data/app/views/headmin/views/devise/passwords/_edit.html.erb +2 -2
- data/app/views/headmin/views/devise/passwords/_new.html.erb +1 -1
- data/app/views/headmin/views/devise/registrations/_edit.html.erb +4 -4
- data/app/views/headmin/views/devise/registrations/_new.html.erb +3 -3
- data/app/views/headmin/views/devise/sessions/_new.html.erb +3 -3
- data/app/views/headmin/views/devise/unlocks/_new.html.erb +1 -1
- data/config/locales/en.yml +4 -0
- data/config/locales/headmin/dropdown/en.yml +6 -0
- data/config/locales/headmin/dropdown/nl.yml +6 -0
- data/config/locales/headmin/filters/en.yml +26 -1
- data/config/locales/headmin/filters/nl.yml +26 -1
- data/config/locales/headmin/layout/en.yml +0 -9
- data/config/locales/headmin/layout/nl.yml +0 -9
- data/config/locales/headmin/nav/en.yml +7 -0
- data/config/locales/headmin/nav/nl.yml +7 -0
- data/config/locales/nl.yml +4 -0
- data/lib/generators/templates/views/layouts/auth.html.erb +1 -1
- data/lib/headmin/version.rb +1 -1
- data/package.json +1 -1
- metadata +47 -7
- data/app/controllers/concerns/headmin/filter.rb +0 -5
- data/app/controllers/concerns/headmin/searchable.rb +0 -15
- data/app/views/headmin/filters/_select.html.erb +0 -45
- data/app/views/headmin/filters/filter/_template.html.erb +0 -13
- data/app/views/headmin/forms/fields/_image.html.erb +0 -17
@@ -6,68 +6,162 @@ module Headmin
|
|
6
6
|
has_many :fields, as: :fieldable, dependent: :destroy
|
7
7
|
accepts_nested_attributes_for :fields, allow_destroy: true
|
8
8
|
|
9
|
+
# Callbacks
|
10
|
+
before_validation :build_fields
|
11
|
+
|
9
12
|
def fields_hash
|
10
|
-
|
11
|
-
parse_hash_tree(field.hash_tree)
|
12
|
-
end.reduce({}, :merge)
|
13
|
+
@fields_hash ||= parse_fields
|
13
14
|
end
|
14
15
|
|
15
16
|
def fields_hash=(hash)
|
16
|
-
|
17
|
+
@fields_hash = hash
|
17
18
|
end
|
18
19
|
|
19
20
|
private
|
20
21
|
|
21
|
-
def
|
22
|
+
def parse_fields
|
23
|
+
fields.where(parent: nil).order(position: :asc).map { |field| parse_hash_tree(field.hash_tree) }.reduce({}, :merge)
|
24
|
+
end
|
25
|
+
|
26
|
+
def build_fields
|
27
|
+
return unless @fields_hash
|
28
|
+
self.fields = fields_for(@fields_hash)
|
29
|
+
end
|
30
|
+
|
31
|
+
def fields_for(hash)
|
22
32
|
hash.map do |key, value|
|
23
33
|
case value
|
24
34
|
when Hash
|
25
|
-
|
26
|
-
name: key,
|
27
|
-
field_type: "group",
|
28
|
-
fields: parse_fields_hash(value)
|
29
|
-
)
|
35
|
+
field_for_hash(key, value)
|
30
36
|
when Array
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
fields: value.map do |item|
|
35
|
-
fields.new(name: "item", field_type: "group", fields: parse_fields_hash(item))
|
36
|
-
end
|
37
|
-
)
|
38
|
-
when String
|
39
|
-
fields.build(
|
40
|
-
name: key,
|
41
|
-
field_type: "text",
|
42
|
-
value: value
|
43
|
-
)
|
37
|
+
field_for_array(key, value)
|
38
|
+
when File
|
39
|
+
field_for_file(key, value)
|
44
40
|
else
|
45
|
-
|
46
|
-
name: key,
|
47
|
-
field_type: "file",
|
48
|
-
file: value
|
49
|
-
)
|
41
|
+
field_for_string(key, value)
|
50
42
|
end
|
51
43
|
end
|
52
44
|
end
|
53
45
|
|
46
|
+
def field_for_hash(name, hash)
|
47
|
+
::Field.new(
|
48
|
+
fieldable: self,
|
49
|
+
name: name,
|
50
|
+
field_type: "group",
|
51
|
+
fields: fields_for(hash)
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
def field_for_array(name, array)
|
56
|
+
if array.all? { |item| item.is_a?(File) }
|
57
|
+
field_for_files(name, array)
|
58
|
+
elsif array.all? { |item| item.is_a?(Hash) }
|
59
|
+
field_for_group_list(name, array)
|
60
|
+
else
|
61
|
+
field_for_simple_list(name, array)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def field_for_simple_list(name, array)
|
66
|
+
::Field.new(
|
67
|
+
fieldable: self,
|
68
|
+
name: name,
|
69
|
+
field_type: "list",
|
70
|
+
fields: array.map { |item| fields_for({item: item}) }.flatten
|
71
|
+
)
|
72
|
+
end
|
73
|
+
|
74
|
+
def field_for_group_list(name, array)
|
75
|
+
::Field.new(
|
76
|
+
fieldable: self,
|
77
|
+
name: name,
|
78
|
+
field_type: "list",
|
79
|
+
fields: array.map { |item| ::Field.new(fieldable: self, name: "item", field_type: "group", fields: fields_for(item)) }
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
def field_for_string(name, string)
|
84
|
+
::Field.new(
|
85
|
+
fieldable: self,
|
86
|
+
name: name,
|
87
|
+
field_type: "text",
|
88
|
+
value: string
|
89
|
+
)
|
90
|
+
end
|
91
|
+
|
92
|
+
def field_for_file(name, file)
|
93
|
+
::Field.new(
|
94
|
+
fieldable: self,
|
95
|
+
name: name,
|
96
|
+
field_type: "file",
|
97
|
+
value: nil,
|
98
|
+
files: [ActiveStorage::Blob.create_and_upload!(
|
99
|
+
io: file,
|
100
|
+
filename: File.basename(file.path)
|
101
|
+
)]
|
102
|
+
)
|
103
|
+
end
|
104
|
+
|
105
|
+
def field_for_files(name, files)
|
106
|
+
::Field.new(
|
107
|
+
fieldable: self,
|
108
|
+
name: name,
|
109
|
+
field_type: "files",
|
110
|
+
value: nil,
|
111
|
+
files: files.map { |file|
|
112
|
+
ActiveStorage::Blob.create_and_upload!(
|
113
|
+
io: file,
|
114
|
+
filename: File.basename(file.path)
|
115
|
+
)
|
116
|
+
}
|
117
|
+
)
|
118
|
+
end
|
119
|
+
|
120
|
+
# From hash tree to hash
|
54
121
|
def parse_hash_tree(hash_tree)
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
122
|
+
new_hash = {}
|
123
|
+
hash_tree.each do |field, children|
|
124
|
+
new_hash[field.name.to_sym] = parse_field(field, children)
|
125
|
+
end
|
126
|
+
new_hash
|
127
|
+
end
|
128
|
+
|
129
|
+
# Parse value for given field
|
130
|
+
def parse_field(field, children)
|
131
|
+
case field.field_type.to_sym
|
132
|
+
when :group
|
133
|
+
parse_group_field(field, children)
|
134
|
+
when :list
|
135
|
+
parse_list_field(field, children)
|
136
|
+
when :files
|
137
|
+
parse_files_field(field)
|
138
|
+
when :file
|
139
|
+
parse_file_field(field)
|
140
|
+
else
|
141
|
+
parse_text_field(field)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def parse_group_field(field, children)
|
146
|
+
parse_hash_tree(children)
|
147
|
+
end
|
148
|
+
|
149
|
+
def parse_list_field(field, children)
|
150
|
+
children.map do |child, grand_children|
|
151
|
+
parse_field(child, grand_children)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def parse_files_field(field)
|
156
|
+
field.files.all
|
157
|
+
end
|
158
|
+
|
159
|
+
def parse_file_field(field)
|
160
|
+
field.files.last
|
161
|
+
end
|
162
|
+
|
163
|
+
def parse_text_field(field)
|
164
|
+
field.value
|
71
165
|
end
|
72
166
|
end
|
73
167
|
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
module Headmin
|
2
|
+
module Filter
|
3
|
+
class Base
|
4
|
+
# Constants
|
5
|
+
OPERATORS = %w[eq not_eq gt gteq lt lteq between not_between in not_in matches does_not_match is_null is_not_null starts_with ends_with]
|
6
|
+
|
7
|
+
OPERATORS_CONVERT_TO = {
|
8
|
+
convert_to_range: %w[between not_between],
|
9
|
+
convert_to_array: %w[in not_in],
|
10
|
+
convert_to_value: %w[eq not_eq gt gteq lt lteq matches does_not_match is_null is_not_null starts_with ends_with]
|
11
|
+
}
|
12
|
+
|
13
|
+
QUERY_VALUE_CONVERT_TO = {
|
14
|
+
convert_value_to_match: %w[matches does_not_match],
|
15
|
+
convert_to_starts: %w[starts_with],
|
16
|
+
convert_to_ends: %w[ends_with]
|
17
|
+
}
|
18
|
+
|
19
|
+
QUERY_OPERATOR_CONVERT_TO = {
|
20
|
+
matches: %w[starts_with ends_with]
|
21
|
+
}
|
22
|
+
|
23
|
+
# Methods
|
24
|
+
def initialize(attribute, params)
|
25
|
+
@raw_value = params[attribute]
|
26
|
+
@attribute = attribute
|
27
|
+
@instructions = []
|
28
|
+
|
29
|
+
if params.key?(attribute)
|
30
|
+
parse(@raw_value)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :raw_value
|
35
|
+
attr_reader :attribute
|
36
|
+
|
37
|
+
def values
|
38
|
+
@instructions.map { |instruction| instruction[:value] }.compact
|
39
|
+
end
|
40
|
+
|
41
|
+
def operators
|
42
|
+
@instructions.map { |instruction| instruction[:operator] }.compact
|
43
|
+
end
|
44
|
+
|
45
|
+
def conditionals
|
46
|
+
@instructions.map { |instruction| instruction[:conditional] }.compact
|
47
|
+
end
|
48
|
+
|
49
|
+
def string(display_values = [])
|
50
|
+
build_instructions_string(display_values)
|
51
|
+
end
|
52
|
+
|
53
|
+
attr_reader :instructions
|
54
|
+
|
55
|
+
def query(collection)
|
56
|
+
return collection unless @instructions.any?
|
57
|
+
|
58
|
+
query = nil
|
59
|
+
|
60
|
+
@instructions.each do |instruction|
|
61
|
+
query = build_query(query, collection, instruction)
|
62
|
+
end
|
63
|
+
|
64
|
+
collection.where(query)
|
65
|
+
end
|
66
|
+
|
67
|
+
def cast_value(value)
|
68
|
+
# This method has to be implemented by the child.
|
69
|
+
|
70
|
+
raise NotImplementedMethodError, "Method cast_value has to be implemented by its child."
|
71
|
+
end
|
72
|
+
|
73
|
+
def display_value(value)
|
74
|
+
value
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def parse(string)
|
80
|
+
instructions = split_instructions(string)
|
81
|
+
parse_instructions(instructions)
|
82
|
+
end
|
83
|
+
|
84
|
+
def split_instructions(string)
|
85
|
+
# Regex: It divides the string into separate parts:
|
86
|
+
# - Parts starting with +AND+ OR +OR+ until the next occurrence of +
|
87
|
+
# - First part of the string till the first occurrence of +
|
88
|
+
string.scan(/\+AND\+[^+]* | \+OR\+[^+]* | ^[^+]*/x)
|
89
|
+
end
|
90
|
+
|
91
|
+
def parse_instructions(instructions)
|
92
|
+
instructions.map { |instruction| parse_instruction(instruction) }
|
93
|
+
end
|
94
|
+
|
95
|
+
def parse_instruction(string)
|
96
|
+
conditional_raw, operator_raw, value_raw = split_instruction(string)
|
97
|
+
|
98
|
+
conditional = parse_conditional(conditional_raw)
|
99
|
+
operator = parse_operator(operator_raw, value_raw)
|
100
|
+
value = parse_value(value_raw, operator)
|
101
|
+
|
102
|
+
@instructions << {conditional: conditional, operator: operator, value: value}
|
103
|
+
end
|
104
|
+
|
105
|
+
def split_instruction(string)
|
106
|
+
# Regex: takes out the +AND+ / +OR+, if not found returns nil
|
107
|
+
conditional_raw = string.scan(/\+AND\+|\+OR\+/)[0]
|
108
|
+
|
109
|
+
string = string.remove("+AND+").remove("+OR+")
|
110
|
+
|
111
|
+
# Regex: takes out the value before the : character
|
112
|
+
operator_raw = string.match(/^[^:]*/)[0] == string ? nil : string.match(/^[^:]*/)[0]
|
113
|
+
value_raw = string.remove("#{operator_raw}:")
|
114
|
+
|
115
|
+
[conditional_raw, operator_raw, value_raw]
|
116
|
+
end
|
117
|
+
|
118
|
+
def parse_conditional(string)
|
119
|
+
string.present? ? string.delete("+").downcase : nil
|
120
|
+
end
|
121
|
+
|
122
|
+
def parse_operator(string, value)
|
123
|
+
if self.class::OPERATORS.include?(string)
|
124
|
+
# Operator is recognized, return the operator
|
125
|
+
string
|
126
|
+
elsif string.nil?
|
127
|
+
# There was no operator present, default operator is eq
|
128
|
+
"eq"
|
129
|
+
else
|
130
|
+
# There was an operator passed but it was not recognized, raise exception
|
131
|
+
raise OperatorNotSupportedError, "This operator is not supported by this filter."
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def parse_value(string, operator)
|
136
|
+
if operator == "is_null" || operator == "is_not_null"
|
137
|
+
return string.to_i
|
138
|
+
end
|
139
|
+
process_value(string, operator)
|
140
|
+
end
|
141
|
+
|
142
|
+
def build_query(query, collection, instruction)
|
143
|
+
query_operator = convert_to_query_operator(instruction[:operator])
|
144
|
+
query_value = convert_to_query_value(instruction[:value], instruction[:operator])
|
145
|
+
|
146
|
+
query_operator, query_value = process_null_operators(query_operator, query_value)
|
147
|
+
|
148
|
+
model = collection.is_a?(Class) ? collection : collection.model
|
149
|
+
new_query = model.arel_table[attribute].send(query_operator, query_value)
|
150
|
+
|
151
|
+
query ? query.send(instruction[:conditional], new_query) : new_query
|
152
|
+
end
|
153
|
+
|
154
|
+
def process_null_operators(operator, value)
|
155
|
+
# In case of null operators (is_null and is_not_null), we have to intercept the operator and value values
|
156
|
+
# and transform them to the correct operator (eq or not_eq) and value (nil)
|
157
|
+
if operator == "is_null" && value == 1
|
158
|
+
["eq", nil]
|
159
|
+
elsif operator == "is_null" && value == 0
|
160
|
+
["not_eq", nil]
|
161
|
+
elsif operator == "is_not_null" && value == 1
|
162
|
+
["not_eq", nil]
|
163
|
+
elsif operator == "is_not_null" && value == 0
|
164
|
+
["eq", nil]
|
165
|
+
else
|
166
|
+
[operator, value]
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def convert_to_range(value)
|
171
|
+
array = value.split(",")
|
172
|
+
cast_value(array[0])..cast_value(array[1])
|
173
|
+
end
|
174
|
+
|
175
|
+
def convert_to_array(value)
|
176
|
+
array = value.split(",")
|
177
|
+
array.map { |e| cast_value(e) }
|
178
|
+
end
|
179
|
+
|
180
|
+
def convert_to_value(value)
|
181
|
+
cast_value(value)
|
182
|
+
end
|
183
|
+
|
184
|
+
def convert_to_query_value(value, operator)
|
185
|
+
value_converter = self.class::QUERY_VALUE_CONVERT_TO.find { |key, values| values.include?(operator) }&.first
|
186
|
+
|
187
|
+
if value_converter.present?
|
188
|
+
send(value_converter, value)
|
189
|
+
else
|
190
|
+
value
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def convert_value_to_match(value)
|
195
|
+
"%#{value}%"
|
196
|
+
end
|
197
|
+
|
198
|
+
def convert_to_query_operator(operator)
|
199
|
+
self.class::QUERY_OPERATOR_CONVERT_TO.find { |key, values| values.include?(operator) }&.first || operator
|
200
|
+
end
|
201
|
+
|
202
|
+
def match_operator_with_convert(operator)
|
203
|
+
self.class::OPERATORS_CONVERT_TO.find { |key, values| values.include?(operator) }&.first
|
204
|
+
end
|
205
|
+
|
206
|
+
def process_value(value, operator)
|
207
|
+
send(match_operator_with_convert(operator), value)
|
208
|
+
end
|
209
|
+
|
210
|
+
def build_instructions_string(display_values)
|
211
|
+
strings = @instructions.map { |instruction| build_instruction_string(instruction, display_values) }
|
212
|
+
strings.join(" ")
|
213
|
+
end
|
214
|
+
|
215
|
+
def build_instruction_string(instruction, display_values)
|
216
|
+
conditional = instruction[:conditional].present? ? "#{I18n.t("headmin.filters.conditionals.#{instruction[:conditional]}")} " : nil
|
217
|
+
operator = I18n.t("headmin.filters.operators.#{instruction[:operator]}")
|
218
|
+
value = display_values[:display_values].find { |item| item.second == instruction[:value] }
|
219
|
+
value = value.present? ? value.first : instruction[:value]
|
220
|
+
|
221
|
+
value = if instruction[:operator] == "is_null" || instruction[:operator] == "is_not_null"
|
222
|
+
# In case of special operators (is_null & is_not_null), we intercept the value
|
223
|
+
value == 1 ? I18n.t("headmin.filters.values.yes") : I18n.t("headmin.filters.values.no")
|
224
|
+
else
|
225
|
+
display_value(value)
|
226
|
+
end
|
227
|
+
|
228
|
+
"#{conditional}#{operator} “#{value}”"
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
class OperatorNotSupportedError < StandardError
|
233
|
+
end
|
234
|
+
|
235
|
+
class NotImplementedMethodError < StandardError
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Headmin
|
2
|
+
module Filter
|
3
|
+
class BaseView < ViewModel
|
4
|
+
def menu_item_options
|
5
|
+
keys = %i[name label]
|
6
|
+
options = to_h.slice(*keys)
|
7
|
+
default_menu_item_options.merge(options)
|
8
|
+
end
|
9
|
+
|
10
|
+
def filter_template_options
|
11
|
+
keys = %i[name label]
|
12
|
+
options = to_h.slice(*keys)
|
13
|
+
default_filter_template_options.merge(options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def filter_operator_options
|
17
|
+
keys = %i[allowed_operators]
|
18
|
+
options = to_h.slice(*keys)
|
19
|
+
default_filter_operator_options.merge(options)
|
20
|
+
end
|
21
|
+
|
22
|
+
def filter_button_options
|
23
|
+
keys = %i[name label display_values filter]
|
24
|
+
options = to_h.slice(*keys)
|
25
|
+
default_filter_button_options.merge(options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def label
|
29
|
+
@label || name.to_s.humanize
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def default_menu_item_options
|
35
|
+
{
|
36
|
+
name: nil,
|
37
|
+
label: label
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def default_filter_template_options
|
42
|
+
{
|
43
|
+
name: nil,
|
44
|
+
label: label
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def default_filter_operator_options
|
49
|
+
{
|
50
|
+
allowed_operators: []
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
def default_filter_button_options
|
55
|
+
{
|
56
|
+
name: nil,
|
57
|
+
label: label,
|
58
|
+
display_values: [],
|
59
|
+
filter: nil
|
60
|
+
}
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Headmin
|
2
|
+
module Filter
|
3
|
+
class BooleanView < ViewModel
|
4
|
+
def base_options
|
5
|
+
keys = %i[name label form]
|
6
|
+
options = to_h.slice(*keys)
|
7
|
+
default_base_options.merge(options)
|
8
|
+
end
|
9
|
+
|
10
|
+
def input_options
|
11
|
+
keys = %i[form]
|
12
|
+
options = to_h.slice(*keys)
|
13
|
+
default_input_options.merge(options)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def value
|
19
|
+
# If the value is nil we want to display the first option (which is true)
|
20
|
+
@value.nil? ? @value = true : ""
|
21
|
+
end
|
22
|
+
|
23
|
+
def id
|
24
|
+
"#{name}_value"
|
25
|
+
end
|
26
|
+
|
27
|
+
def name
|
28
|
+
@name || attribute
|
29
|
+
end
|
30
|
+
|
31
|
+
def label
|
32
|
+
@label || I18n.t("attributes.#{attribute}", default: name.to_s)
|
33
|
+
end
|
34
|
+
|
35
|
+
def default_base_options
|
36
|
+
{
|
37
|
+
label: label,
|
38
|
+
name: attribute,
|
39
|
+
filter: Headmin::Filter::Boolean.new(name, @params),
|
40
|
+
display_values: [[I18n.t("headmin.filters.values.true"), true], [I18n.t("headmin.filters.values.false"), false]],
|
41
|
+
allowed_operators: Headmin::Filter::Boolean::OPERATORS
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def default_input_options
|
46
|
+
{
|
47
|
+
label: false,
|
48
|
+
wrapper: false,
|
49
|
+
id: id,
|
50
|
+
collection: [[I18n.t("headmin.filters.values.true"), 1], [I18n.t("headmin.filters.values.false"), 0]],
|
51
|
+
selected: value ? 1 : 0,
|
52
|
+
data: {
|
53
|
+
action: "change->filter#updateHiddenValue",
|
54
|
+
filter_target: "value",
|
55
|
+
filter_row_target: "original"
|
56
|
+
}
|
57
|
+
}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Headmin
|
2
|
+
module Filter
|
3
|
+
class ButtonView < ViewModel
|
4
|
+
def id
|
5
|
+
@id || SecureRandom.hex
|
6
|
+
end
|
7
|
+
|
8
|
+
def name
|
9
|
+
@name.to_sym
|
10
|
+
end
|
11
|
+
|
12
|
+
def label
|
13
|
+
@label || name.to_s.humanize
|
14
|
+
end
|
15
|
+
|
16
|
+
def display_values
|
17
|
+
@display_values || []
|
18
|
+
end
|
19
|
+
|
20
|
+
def filter
|
21
|
+
@filter || nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Headmin
|
2
|
+
module Filter
|
3
|
+
class ConditionalView < ViewModel
|
4
|
+
def selected
|
5
|
+
@selected ? "+#{@selected.to_s.upcase}+" : nil
|
6
|
+
end
|
7
|
+
|
8
|
+
def values
|
9
|
+
{
|
10
|
+
"+AND+": I18n.t("headmin.filters.filter.conditional.and"),
|
11
|
+
"+OR+": I18n.t("headmin.filters.filter.conditional.or")
|
12
|
+
}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Headmin
|
2
|
+
module Filter
|
3
|
+
class Date < Headmin::Filter::Base
|
4
|
+
OPERATORS = %w[eq not_eq gt gteq lt lteq between not_between in not_in is_null is_not_null]
|
5
|
+
|
6
|
+
def cast_value(value)
|
7
|
+
value.present? ? value.to_date : ::Date.current
|
8
|
+
rescue
|
9
|
+
raise TypeError, "The value that was passed to this filter is not a date"
|
10
|
+
end
|
11
|
+
|
12
|
+
def display_value(value)
|
13
|
+
# This uses the default date format of headmin.
|
14
|
+
# Can be overwritten by setting default date format of the application.
|
15
|
+
I18n.l(value)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|