headmin 0.4.0 → 0.5.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.
- 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/autocomplete_controller.js +84 -21
- 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 +14 -5
- 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 +186 -56
- data/app/assets/stylesheets/headmin/filter.scss +74 -0
- data/app/assets/stylesheets/headmin/general.scss +8 -0
- data/app/assets/stylesheets/headmin/layout/body.scss +5 -0
- data/app/assets/stylesheets/headmin/popup.scss +0 -1
- data/app/assets/stylesheets/headmin.css +70 -1
- 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/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/blocks_view.rb +1 -1
- data/app/models/headmin/form/checkbox_view.rb +3 -3
- data/app/models/headmin/form/date_range_view.rb +2 -2
- data/app/models/headmin/form/date_view.rb +5 -5
- 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/email_view.rb +7 -7
- data/app/models/headmin/form/file_view.rb +6 -6
- data/app/models/headmin/form/flatpickr_range_view.rb +11 -22
- data/app/models/headmin/form/flatpickr_view.rb +4 -13
- data/app/models/headmin/form/input_group_view.rb +1 -1
- data/app/models/headmin/form/label_view.rb +1 -1
- data/app/models/headmin/form/number_view.rb +5 -5
- data/app/models/headmin/form/password_view.rb +5 -5
- data/app/models/headmin/form/redactorx_view.rb +2 -2
- data/app/models/headmin/form/search_view.rb +7 -7
- data/app/models/headmin/form/select_view.rb +6 -6
- data/app/models/headmin/form/switch_view.rb +1 -1
- data/app/models/headmin/form/text_view.rb +7 -7
- data/app/models/headmin/form/textarea_view.rb +5 -5
- data/app/models/headmin/form/url_view.rb +7 -7
- data/app/models/headmin/form/wrapper_view.rb +1 -1
- data/app/models/headmin/form/wysiwyg_view.rb +1 -1
- 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/_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/_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 +10 -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/_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/forms/en.yml +1 -1
- data/config/locales/headmin/forms/nl.yml +1 -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 +44 -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
@@ -13104,6 +13104,9 @@ input[type=search] {
|
|
13104
13104
|
html {
|
13105
13105
|
height: 100%;
|
13106
13106
|
}
|
13107
|
+
.list-group-item.active .text-muted {
|
13108
|
+
color: #fff !important;
|
13109
|
+
}
|
13107
13110
|
mark,
|
13108
13111
|
.mark {
|
13109
13112
|
padding: 0;
|
@@ -13113,6 +13116,11 @@ mark,
|
|
13113
13116
|
background: rgba(0, 0, 0, 0.03);
|
13114
13117
|
height: 100vh;
|
13115
13118
|
overflow-y: scroll;
|
13119
|
+
display: flex;
|
13120
|
+
flex-direction: column;
|
13121
|
+
}
|
13122
|
+
.content {
|
13123
|
+
flex-grow: 1;
|
13116
13124
|
}
|
13117
13125
|
.sidebar {
|
13118
13126
|
font-size: 0.99rem;
|
@@ -13306,6 +13314,54 @@ body.empty {
|
|
13306
13314
|
.h-filter-popup.closed {
|
13307
13315
|
display: none;
|
13308
13316
|
}
|
13317
|
+
.h-filter-row {
|
13318
|
+
padding: 0.5rem 0 0.5rem 0;
|
13319
|
+
position: relative;
|
13320
|
+
flex-wrap: unset;
|
13321
|
+
display: flex;
|
13322
|
+
gap: 10px;
|
13323
|
+
}
|
13324
|
+
.h-filter-row:first-child {
|
13325
|
+
padding-top: 0;
|
13326
|
+
}
|
13327
|
+
.h-filter-row:hover .h-filter-add-input,
|
13328
|
+
.h-filter-row:hover .h-filter-remove-input {
|
13329
|
+
display: block;
|
13330
|
+
}
|
13331
|
+
.h-filter-row input,
|
13332
|
+
.h-filter-row select:not(.h-filter-operator) {
|
13333
|
+
min-width: 200px;
|
13334
|
+
}
|
13335
|
+
.h-filter-operator {
|
13336
|
+
width: 62px;
|
13337
|
+
}
|
13338
|
+
.h-filter-conditional {
|
13339
|
+
position: relative;
|
13340
|
+
display: flex;
|
13341
|
+
justify-content: center;
|
13342
|
+
align-items: center;
|
13343
|
+
}
|
13344
|
+
.h-filter-conditional select {
|
13345
|
+
width: 50px;
|
13346
|
+
}
|
13347
|
+
.h-filter-conditional::before {
|
13348
|
+
content: "";
|
13349
|
+
position: absolute;
|
13350
|
+
top: calc(50% - 1px);
|
13351
|
+
left: 0;
|
13352
|
+
width: calc(50% - 35px);
|
13353
|
+
height: 1px;
|
13354
|
+
background: rgba(0, 0, 0, 0.2);
|
13355
|
+
}
|
13356
|
+
.h-filter-conditional::after {
|
13357
|
+
content: "";
|
13358
|
+
position: absolute;
|
13359
|
+
top: calc(50% - 1px);
|
13360
|
+
left: calc(50% + 35px);
|
13361
|
+
width: calc(50% - 35px);
|
13362
|
+
height: 1px;
|
13363
|
+
background: rgba(0, 0, 0, 0.2);
|
13364
|
+
}
|
13309
13365
|
.h-filter-remove {
|
13310
13366
|
border-left: 1px solid #d1d5db;
|
13311
13367
|
padding-left: 8px;
|
@@ -13314,11 +13370,24 @@ body.empty {
|
|
13314
13370
|
.h-filter-remove i::before {
|
13315
13371
|
font-size: 0.8em;
|
13316
13372
|
}
|
13373
|
+
.h-filter-add-input {
|
13374
|
+
position: absolute !important;
|
13375
|
+
top: calc(100% - 12px);
|
13376
|
+
left: calc(50% - 17px);
|
13377
|
+
z-index: 9;
|
13378
|
+
display: none;
|
13379
|
+
}
|
13380
|
+
.h-filter-remove-input {
|
13381
|
+
position: absolute !important;
|
13382
|
+
top: calc(50% - 15px);
|
13383
|
+
left: calc(100% - 4px);
|
13384
|
+
z-index: 9;
|
13385
|
+
display: none;
|
13386
|
+
}
|
13317
13387
|
.h-popup {
|
13318
13388
|
padding: 10px;
|
13319
13389
|
background: #ffffff;
|
13320
13390
|
z-index: 1070;
|
13321
|
-
width: 276px;
|
13322
13391
|
font-family: var(--bs-font-sans-serif);
|
13323
13392
|
font-style: normal;
|
13324
13393
|
font-weight: 400;
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Headmin
|
2
|
+
module Filterable
|
3
|
+
# Will create a Headmin::Filters object with a default configuration for "id" and "search"
|
4
|
+
#
|
5
|
+
# Example:
|
6
|
+
#
|
7
|
+
# orders = Order
|
8
|
+
# orders = filter(orders, {
|
9
|
+
# status: :text,
|
10
|
+
# price: :number,
|
11
|
+
# in_stock: :boolean
|
12
|
+
# })
|
13
|
+
# @orders = orders.all
|
14
|
+
|
15
|
+
def filter(collection, filter_types = {})
|
16
|
+
type_hash = default_filter_types.merge(filter_types)
|
17
|
+
Headmin::Filters.new(params, type_hash).query(collection)
|
18
|
+
end
|
19
|
+
|
20
|
+
def default_filter_types
|
21
|
+
{
|
22
|
+
id: :number,
|
23
|
+
search: :search
|
24
|
+
}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -11,8 +11,10 @@ module Headmin
|
|
11
11
|
belongs_to :field, optional: true, touch: true
|
12
12
|
has_many :fields, foreign_key: "parent_id"
|
13
13
|
accepts_nested_attributes_for :fields, allow_destroy: true
|
14
|
-
|
15
|
-
|
14
|
+
|
15
|
+
# field_type: :files, :file
|
16
|
+
has_many_attached :files
|
17
|
+
accepts_nested_attributes_for :files_attachments, allow_destroy: true
|
16
18
|
end
|
17
19
|
end
|
18
20
|
end
|
@@ -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
|