headmin 0.5.4 → 0.5.6

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -2
  3. data/app/assets/javascripts/headmin/controllers/media_controller.js +1 -39
  4. data/app/assets/javascripts/headmin/controllers/repeater_controller.js +16 -4
  5. data/app/assets/javascripts/headmin.js +10 -33
  6. data/app/controllers/concerns/headmin/pagination.rb +5 -1
  7. data/app/controllers/headmin/media_controller.rb +8 -3
  8. data/app/models/concerns/headmin/blob.rb +36 -0
  9. data/app/models/concerns/headmin/field.rb +1 -1
  10. data/app/models/concerns/headmin/fieldable.rb +4 -1
  11. data/app/models/headmin/filter/association_view.rb +1 -1
  12. data/app/models/headmin/filter/base.rb +34 -17
  13. data/app/models/headmin/filter/boolean_view.rb +1 -5
  14. data/app/models/headmin/filter/date_view.rb +1 -5
  15. data/app/models/headmin/filter/field.rb +55 -0
  16. data/app/models/headmin/filter/field_view.rb +50 -0
  17. data/app/models/headmin/filter/filter_view.rb +25 -0
  18. data/app/models/headmin/filter/number_view.rb +1 -5
  19. data/app/models/headmin/filter/options_view.rb +1 -5
  20. data/app/models/headmin/filter/text_view.rb +1 -5
  21. data/app/models/headmin/filters.rb +19 -4
  22. data/app/models/headmin/form/media_item_view.rb +4 -0
  23. data/app/models/headmin/form/media_view.rb +5 -1
  24. data/app/views/headmin/_filters.html.erb +1 -1
  25. data/app/views/headmin/filters/_field.html.erb +23 -0
  26. data/app/views/headmin/forms/_media.html.erb +3 -5
  27. data/app/views/headmin/media/_modal.html.erb +1 -1
  28. data/app/views/headmin/media/index.html.erb +1 -1
  29. data/config/initializers/extend_active_storage_blob.rb +3 -0
  30. data/config/locales/headmin/forms/en.yml +0 -8
  31. data/config/locales/headmin/forms/nl.yml +0 -8
  32. data/lib/headmin/version.rb +1 -1
  33. data/package.json +1 -1
  34. metadata +8 -3
  35. data/app/views/headmin/forms/media/_validation.html.erb +0 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c163e16eb068c93256e3a73a0ac3b58f569cc72c36da590f084ecb13873b8866
4
- data.tar.gz: 9297b7bfd0194980aa843c45e59f9180349bc17e571cfe8f9c75678738de7def
3
+ metadata.gz: 7f5b560711076cca763871af2499728101e4f8d7a27a04f5c702671165e134d7
4
+ data.tar.gz: ed2d18ec0405786fd9b4f18984e750571062124828fc1102a662f78e0333c03e
5
5
  SHA512:
6
- metadata.gz: b72bc6a71c864837e78619f8bfc7b0d455d4f57d108790f6d6619fbb3fe2acb882d2be87236b8a2d87204b55995fd4234c8617ee93be5a587b6eafa2616aaac4
7
- data.tar.gz: 819ec0caa24dbfa82ac02dec7909d6c888550f87c71bf5eadae0b86b03a179c57b599c5a3af3b418e4435dfc569373a1b744ed5621380382bdf7cff4cb6d229d
6
+ metadata.gz: 8afa230d5e31dad3cbaa288fe46849672af9c4df118b4e9a9ed8024eea60da7d1089865b3678ee67860cfe88d34f67ba3a3db0c2b8ee1e37ebfbe515e2ae07fc
7
+ data.tar.gz: 106023130df278d025a608345e763aac87b6586736e511fd05866703876b296de9ae08d43ea5fd86d3f7cb75d378935e3aa86ff2d9f519d50434ee02e6453dc8
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- headmin (0.5.3)
4
+ headmin (0.5.5)
5
5
  closure_tree (~> 7.4)
6
6
  inline_svg (~> 1.7)
7
7
  redcarpet (~> 3.5)
@@ -226,7 +226,7 @@ GEM
226
226
  actionpack (>= 5.0)
227
227
  railties (>= 5.0)
228
228
  rexml (3.2.5)
229
- rouge (3.29.0)
229
+ rouge (3.30.0)
230
230
  route_translator (12.1.0)
231
231
  actionpack (>= 5.2, < 7.1)
232
232
  activesupport (>= 5.2, < 7.1)
@@ -3,7 +3,7 @@ import Sortable from 'sortablejs'
3
3
 
4
4
  export default class extends Controller {
5
5
  static get targets () {
6
- return ['item', 'template', 'thumbnails', 'modalButton', 'placeholder', 'validationInput', 'count', 'editButton']
6
+ return ['item', 'template', 'thumbnails', 'modalButton', 'placeholder', 'count', 'editButton']
7
7
  }
8
8
 
9
9
  connect () {
@@ -17,8 +17,6 @@ export default class extends Controller {
17
17
  if (this.hasSorting()) {
18
18
  this.initSortable()
19
19
  }
20
-
21
- this.validate()
22
20
  }
23
21
 
24
22
  // Actions
@@ -83,42 +81,6 @@ export default class extends Controller {
83
81
 
84
82
  // Toggle placeholder
85
83
  this.togglePlaceholder()
86
-
87
- // Validate
88
- this.validate()
89
- }
90
-
91
- validate () {
92
- this.clearValidation()
93
- if (this.element.dataset.required === '0') return
94
- this.validateMinimum()
95
- this.validateMaximum()
96
- }
97
-
98
- clearValidation () {
99
- this.validationInputTarget.setCustomValidity('')
100
- }
101
-
102
- validateMinimum () {
103
- const count = this.activeItems().length
104
- if (count < this.minActiveItems()) {
105
- this.validationInputTarget.setCustomValidity(this.validationInputTarget.dataset.minMessage)
106
- }
107
- }
108
-
109
- validateMaximum () {
110
- const count = this.activeItems().length
111
- if (count > this.maxActiveItems()) {
112
- this.validationInputTarget.setCustomValidity(this.validationInputTarget.dataset.maxMessage)
113
- }
114
- }
115
-
116
- minActiveItems () {
117
- return parseInt(this.element.dataset.min, 10) || 0
118
- }
119
-
120
- maxActiveItems () {
121
- return parseInt(this.element.dataset.max, 10) || Infinity
122
84
  }
123
85
 
124
86
  resetPositions () {
@@ -80,8 +80,8 @@ export default class extends Controller {
80
80
  row.remove()
81
81
  } else {
82
82
  // Existing records are hidden and flagged for deletion
83
- row.querySelector("input[name*='_destroy']").value = 1
84
- row.style.display = 'none'
83
+ this.flagRowForDeletion(row)
84
+ row.remove()
85
85
  }
86
86
 
87
87
  this.resetIndices()
@@ -89,6 +89,18 @@ export default class extends Controller {
89
89
  this.toggleEmpty()
90
90
  }
91
91
 
92
+ flagRowForDeletion (row) {
93
+ const destroyInput = row.querySelector('input[name*=\'_destroy\']')
94
+ const idInput = row.querySelector('input[name*=\'[id]\']')
95
+
96
+ // Update _destroy value
97
+ destroyInput.value = 1
98
+
99
+ // Move away from row
100
+ this.listTarget.parentNode.appendChild(destroyInput)
101
+ this.listTarget.parentNode.appendChild(idInput)
102
+ }
103
+
92
104
  getTemplate (name) {
93
105
  return this.templateTargets.filter((template) => {
94
106
  return template.dataset.templateName === name
@@ -107,7 +119,7 @@ export default class extends Controller {
107
119
  visibleRows () {
108
120
  const rows = this.rowTargets
109
121
  return rows.filter((row) => {
110
- return row.querySelector("input[name*='_destroy']").value !== '1'
122
+ return row.querySelector('input[name*=\'_destroy\']').value !== '1'
111
123
  })
112
124
  }
113
125
 
@@ -121,7 +133,7 @@ export default class extends Controller {
121
133
 
122
134
  resetPositions () {
123
135
  this.visibleRows().forEach((row, index) => {
124
- const positionInput = row.querySelector("input[name*='position']")
136
+ const positionInput = row.querySelector('input[name*=\'position\']')
125
137
  if (positionInput) {
126
138
  positionInput.value = index
127
139
  }
@@ -10096,7 +10096,7 @@ var hello_controller_default = class extends Controller {
10096
10096
  // app/assets/javascripts/headmin/controllers/media_controller.js
10097
10097
  var media_controller_default = class extends Controller {
10098
10098
  static get targets() {
10099
- return ["item", "template", "thumbnails", "modalButton", "placeholder", "validationInput", "count", "editButton"];
10099
+ return ["item", "template", "thumbnails", "modalButton", "placeholder", "count", "editButton"];
10100
10100
  }
10101
10101
  connect() {
10102
10102
  document.addEventListener("mediaSelectionSubmitted", (event) => {
@@ -10107,7 +10107,6 @@ var media_controller_default = class extends Controller {
10107
10107
  if (this.hasSorting()) {
10108
10108
  this.initSortable();
10109
10109
  }
10110
- this.validate();
10111
10110
  }
10112
10111
  destroy(event) {
10113
10112
  const item = event.currentTarget.closest("[data-media-target='item']");
@@ -10148,35 +10147,6 @@ var media_controller_default = class extends Controller {
10148
10147
  this.resetPositions();
10149
10148
  this.syncIds();
10150
10149
  this.togglePlaceholder();
10151
- this.validate();
10152
- }
10153
- validate() {
10154
- this.clearValidation();
10155
- if (this.element.dataset.required === "0")
10156
- return;
10157
- this.validateMinimum();
10158
- this.validateMaximum();
10159
- }
10160
- clearValidation() {
10161
- this.validationInputTarget.setCustomValidity("");
10162
- }
10163
- validateMinimum() {
10164
- const count = this.activeItems().length;
10165
- if (count < this.minActiveItems()) {
10166
- this.validationInputTarget.setCustomValidity(this.validationInputTarget.dataset.minMessage);
10167
- }
10168
- }
10169
- validateMaximum() {
10170
- const count = this.activeItems().length;
10171
- if (count > this.maxActiveItems()) {
10172
- this.validationInputTarget.setCustomValidity(this.validationInputTarget.dataset.maxMessage);
10173
- }
10174
- }
10175
- minActiveItems() {
10176
- return parseInt(this.element.dataset.min, 10) || 0;
10177
- }
10178
- maxActiveItems() {
10179
- return parseInt(this.element.dataset.max, 10) || Infinity;
10180
10150
  }
10181
10151
  resetPositions() {
10182
10152
  this.activeItems().forEach((item, index2) => {
@@ -15559,13 +15529,20 @@ var repeater_controller_default = class extends Controller {
15559
15529
  if (row.dataset.newRecord === "true") {
15560
15530
  row.remove();
15561
15531
  } else {
15562
- row.querySelector("input[name*='_destroy']").value = 1;
15563
- row.style.display = "none";
15532
+ this.flagRowForDeletion(row);
15533
+ row.remove();
15564
15534
  }
15565
15535
  this.resetIndices();
15566
15536
  this.resetPositions();
15567
15537
  this.toggleEmpty();
15568
15538
  }
15539
+ flagRowForDeletion(row) {
15540
+ const destroyInput = row.querySelector("input[name*='_destroy']");
15541
+ const idInput = row.querySelector("input[name*='[id]']");
15542
+ destroyInput.value = 1;
15543
+ this.listTarget.parentNode.appendChild(destroyInput);
15544
+ this.listTarget.parentNode.appendChild(idInput);
15545
+ }
15569
15546
  getTemplate(name) {
15570
15547
  return this.templateTargets.filter((template) => {
15571
15548
  return template.dataset.templateName === name;
@@ -2,7 +2,11 @@ module Headmin
2
2
  module Pagination
3
3
  def paginate(collection)
4
4
  @records_filtered = collection.count
5
- collection.page(page).per(per_page)
5
+ if collection.is_a?(Array)
6
+ Kaminari.paginate_array(collection).page(page).per(per_page)
7
+ else
8
+ collection.page(page).per(per_page)
9
+ end
6
10
  end
7
11
 
8
12
  def page
@@ -4,12 +4,12 @@ class Headmin::MediaController < HeadminController
4
4
  def index
5
5
  @blobs =
6
6
  ActiveStorage::Blob
7
- .left_outer_joins(:attachments)
8
- .where.not(active_storage_attachments: {record_type: "ActiveStorage::VariantRecord"}) # Not a variant
9
- .or(ActiveStorage::Blob.where(active_storage_attachments: {id: nil})) # Or an orphan
7
+ .not_attached_to_variant
8
+ .by_mimetypes_string(media_params[:mimetype])
10
9
  .order(created_at: :desc)
11
10
  .group(:id)
12
11
  .all
12
+ @mimetypes = media_params[:mimetype]
13
13
  end
14
14
 
15
15
  def create
@@ -42,6 +42,11 @@ class Headmin::MediaController < HeadminController
42
42
 
43
43
  def media_params
44
44
  params.permit(
45
+ :min,
46
+ :max,
47
+ :name,
48
+ :mimetype,
49
+ ids: [],
45
50
  files: []
46
51
  )
47
52
  end
@@ -0,0 +1,36 @@
1
+ module Headmin
2
+ module Blob
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class << self
7
+ def not_attached_to_variant
8
+ left_outer_joins(:attachments)
9
+ .where.not(active_storage_attachments: {record_type: "ActiveStorage::VariantRecord"})
10
+ .or(is_orphan)
11
+ end
12
+
13
+ def is_orphan
14
+ left_outer_joins(:attachments)
15
+ .where(active_storage_attachments: {id: nil})
16
+ end
17
+
18
+ def by_mimetypes_string(mimetype_string)
19
+ by_mimetypes(mimetype_string.split(","))
20
+ end
21
+
22
+ def by_mimetypes(mimetypes = [])
23
+ results = self
24
+
25
+ mimetypes.map.with_index do |mimetype, index|
26
+ content_type = mimetype.tr("*", "%")
27
+ query = where(arel_table[:content_type].matches(content_type))
28
+ results = index == 0 ? query : results.or(query)
29
+ end
30
+
31
+ results
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -4,7 +4,7 @@ module Headmin
4
4
 
5
5
  included do
6
6
  # Configuration
7
- has_closure_tree order: "position", numeric_order: true
7
+ has_closure_tree order: "position", numeric_order: true, dependent: :destroy
8
8
 
9
9
  # Associations
10
10
  belongs_to :fieldable, polymorphic: true, optional: true, touch: true
@@ -28,7 +28,10 @@ module Headmin
28
28
  private
29
29
 
30
30
  def parse_fields
31
- hash_tree = fields.includes(default_include_tables | include_tables).hash_tree
31
+ hash_tree = {}
32
+ fields.roots.includes(default_include_tables | include_tables).each do |field|
33
+ field.hash_tree.map { |key, value| hash_tree[key] = value }
34
+ end
32
35
  parse_hash_tree(hash_tree)
33
36
  end
34
37
 
@@ -1,6 +1,6 @@
1
1
  module Headmin
2
2
  module Filter
3
- class AssociationView < ViewModel
3
+ class AssociationView < FilterView
4
4
  def base_options
5
5
  keys = %i[name label form]
6
6
  options = to_h.slice(*keys)
@@ -21,12 +21,13 @@ module Headmin
21
21
  }
22
22
 
23
23
  # Methods
24
- def initialize(attribute, params)
25
- @raw_value = params[attribute]
26
- @attribute = attribute
24
+ def initialize(attribute, params, association: nil)
25
+ @attribute = association ? "#{association}_#{attribute}".to_sym : attribute
26
+ @raw_value = params[@attribute]
27
+ @association = association
27
28
  @instructions = []
28
29
 
29
- if params.key?(attribute)
30
+ if params.key?(@attribute)
30
31
  parse(@raw_value)
31
32
  end
32
33
  end
@@ -61,7 +62,8 @@ module Headmin
61
62
  query = build_query(query, collection, instruction)
62
63
  end
63
64
 
64
- collection.where(query)
65
+ collection = collection.joins(@association) if @association
66
+ collection.distinct.where(query)
65
67
  end
66
68
 
67
69
  def cast_value(value)
@@ -74,6 +76,30 @@ module Headmin
74
76
  value
75
77
  end
76
78
 
79
+ def build_query(query, collection, instruction)
80
+ query_operator = convert_to_query_operator(instruction[:operator])
81
+ query_value = convert_to_query_value(instruction[:value], instruction[:operator])
82
+
83
+ query_operator, query_value = process_null_operators(query_operator, query_value)
84
+
85
+ model = collection.is_a?(Class) ? collection : collection.model
86
+
87
+ if @association
88
+ # Association attributes are passed through as {association}_{attribute}, so we need to transform this into {attribute}
89
+ new_attribute = attribute.to_s.gsub("#{@association}_", "").to_sym
90
+ new_model = model.reflect_on_association(@association)
91
+
92
+ # In case the association cannot be found, raise a well defined error
93
+ raise UnknownAssociation if new_model.nil?
94
+
95
+ new_query = new_model.klass.arel_table[new_attribute].send(query_operator, query_value)
96
+ else
97
+ new_query = model.arel_table[attribute].send(query_operator, query_value)
98
+ end
99
+
100
+ query ? query.send(instruction[:conditional], new_query) : new_query
101
+ end
102
+
77
103
  private
78
104
 
79
105
  def parse(string)
@@ -139,18 +165,6 @@ module Headmin
139
165
  process_value(string, operator)
140
166
  end
141
167
 
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
168
  def process_null_operators(operator, value)
155
169
  # In case of null operators (is_null and is_not_null), we have to intercept the operator and value values
156
170
  # and transform them to the correct operator (eq or not_eq) and value (nil)
@@ -237,5 +251,8 @@ module Headmin
237
251
 
238
252
  class NotImplementedMethodError < StandardError
239
253
  end
254
+
255
+ class UnknownAssociation < StandardError
256
+ end
240
257
  end
241
258
  end
@@ -1,6 +1,6 @@
1
1
  module Headmin
2
2
  module Filter
3
- class BooleanView < ViewModel
3
+ class BooleanView < FilterView
4
4
  def base_options
5
5
  keys = %i[name label form]
6
6
  options = to_h.slice(*keys)
@@ -28,10 +28,6 @@ module Headmin
28
28
  @name || attribute
29
29
  end
30
30
 
31
- def label
32
- @label || I18n.t("attributes.#{attribute}", default: name.to_s)
33
- end
34
-
35
31
  def default_base_options
36
32
  {
37
33
  label: label,
@@ -1,6 +1,6 @@
1
1
  module Headmin
2
2
  module Filter
3
- class DateView < ViewModel
3
+ class DateView < FilterView
4
4
  def base_options
5
5
  keys = %i[name label form]
6
6
  options = to_h.slice(*keys)
@@ -23,10 +23,6 @@ module Headmin
23
23
  @name || attribute
24
24
  end
25
25
 
26
- def label
27
- @label || I18n.t("attributes.#{attribute}", default: name.to_s)
28
- end
29
-
30
26
  def default_base_options
31
27
  {
32
28
  label: label,
@@ -0,0 +1,55 @@
1
+ module Headmin
2
+ module Filter
3
+ class Field < Headmin::Filter::Base
4
+ OPERATORS = %w[eq not_eq matches does_not_match]
5
+
6
+ def initialize(attribute, params, association: nil)
7
+ @attribute = association ? "#{association}_#{attribute}".to_sym : attribute
8
+ @attribute = "field_#{attribute}".to_sym
9
+ @raw_value = params[@attribute]
10
+ @association = association
11
+ @instructions = []
12
+
13
+ if params.key?(@attribute)
14
+ parse(@raw_value)
15
+ end
16
+ end
17
+
18
+ def cast_value(value)
19
+ value
20
+ end
21
+
22
+ def display_value(value)
23
+ value.downcase
24
+ end
25
+
26
+ def query(collection)
27
+ return collection unless @instructions.any?
28
+
29
+ query = nil
30
+
31
+ @instructions.each do |instruction|
32
+ query = build_query(query, collection, instruction)
33
+ end
34
+
35
+ collection = collection.joins(:fields)
36
+ collection.where(query)
37
+ end
38
+
39
+ def build_query(query, collection, instruction)
40
+ query_operator = convert_to_query_operator(instruction[:operator])
41
+ query_value = convert_to_query_value(instruction[:value], instruction[:operator])
42
+
43
+ query_operator, query_value = process_null_operators(query_operator, query_value)
44
+
45
+ model = collection.is_a?(Class) ? collection : collection.model
46
+
47
+ new_attribute = attribute.to_s.gsub("field_", "").to_sym
48
+ fields_model = model.reflect_on_association(:fields).klass
49
+ new_query = fields_model.arel_table[:name].matches(new_attribute).and(fields_model.arel_table[:value].send(query_operator, query_value))
50
+
51
+ query ? query.send(instruction[:conditional], new_query) : new_query
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,50 @@
1
+ module Headmin
2
+ module Filter
3
+ class FieldView < FilterView
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 id
19
+ "#{name}_value"
20
+ end
21
+
22
+ def name
23
+ "field_#{@name}".to_sym || attribute
24
+ end
25
+
26
+ def default_base_options
27
+ {
28
+ label: label,
29
+ name: "field_#{attribute}".to_sym,
30
+ filter: Headmin::Filter::Field.new(attribute.to_s.to_sym, @params),
31
+ allowed_operators: Headmin::Filter::Field::OPERATORS - %w[in not_in]
32
+ }
33
+ end
34
+
35
+ def default_input_options
36
+ {
37
+ label: false,
38
+ wrapper: false,
39
+ id: id,
40
+ name: nil,
41
+ data: {
42
+ action: "change->filter#updateHiddenValue",
43
+ filter_target: "value",
44
+ filter_row_target: "original"
45
+ }
46
+ }
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,25 @@
1
+ module Headmin
2
+ module Filter
3
+ class FilterView < ViewModel
4
+ def attribute
5
+ @association ? "#{@association}_#{@attribute}".to_sym : @attribute
6
+ end
7
+
8
+ def label
9
+ @label || I18n.t("attributes.#{attribute}", default: @association ? "#{association_model.model_name.human(count: 1)} - #{association_model.human_attribute_name(@attribute)}" : name.to_s)
10
+ end
11
+
12
+ def reflection
13
+ if @association
14
+ form.object.class.reflect_on_association(@association)
15
+ end
16
+ end
17
+
18
+ def association_model
19
+ if @association
20
+ reflection.klass
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,6 +1,6 @@
1
1
  module Headmin
2
2
  module Filter
3
- class NumberView < ViewModel
3
+ class NumberView < FilterView
4
4
  def base_options
5
5
  keys = %i[name label form]
6
6
  options = to_h.slice(*keys)
@@ -23,10 +23,6 @@ module Headmin
23
23
  @name || attribute
24
24
  end
25
25
 
26
- def label
27
- @label || I18n.t("attributes.#{attribute}", default: name.to_s)
28
- end
29
-
30
26
  def default_base_options
31
27
  {
32
28
  label: label,
@@ -1,6 +1,6 @@
1
1
  module Headmin
2
2
  module Filter
3
- class OptionsView < ViewModel
3
+ class OptionsView < FilterView
4
4
  def base_options
5
5
  keys = %i[name label form]
6
6
  options = to_h.slice(*keys)
@@ -27,10 +27,6 @@ module Headmin
27
27
  @name || attribute
28
28
  end
29
29
 
30
- def label
31
- @label || I18n.t("attributes.#{attribute}", default: name.to_s)
32
- end
33
-
34
30
  def default_base_options
35
31
  {
36
32
  label: label,
@@ -1,6 +1,6 @@
1
1
  module Headmin
2
2
  module Filter
3
- class TextView < ViewModel
3
+ class TextView < FilterView
4
4
  def base_options
5
5
  keys = %i[name label form]
6
6
  options = to_h.slice(*keys)
@@ -23,10 +23,6 @@ module Headmin
23
23
  @name || attribute
24
24
  end
25
25
 
26
- def label
27
- @label || I18n.t("attributes.#{attribute}", default: name.to_s)
28
- end
29
-
30
26
  def default_base_options
31
27
  {
32
28
  label: label,
@@ -13,15 +13,30 @@ module Headmin
13
13
  @param_types = param_types
14
14
  end
15
15
 
16
- def parse(attribute, type)
16
+ def parse(attribute, type, association: nil)
17
17
  class_name = "Headmin::Filter::#{type.to_s.classify}".constantize
18
- class_name.new(attribute, @params)
18
+ class_name.new(attribute, @params, association: association)
19
19
  end
20
20
 
21
21
  def query(collection)
22
22
  @param_types.each do |attribute, type|
23
- filter = parse(attribute, type)
24
- collection = filter.query(collection)
23
+ if type.is_a? Hash
24
+ # We are given attribute filters of an association
25
+ association = attribute
26
+
27
+ # By default, we offer a filter of type association
28
+ association_filter = Headmin::Filter::Association.new(attribute, @params, association: nil)
29
+ collection = association_filter.query(collection)
30
+
31
+ # Query all the passed attribute filters for this association
32
+ type.each do |new_attribute, new_type|
33
+ filter = parse(new_attribute, new_type, association: association)
34
+ collection = filter.query(collection)
35
+ end
36
+ else
37
+ filter = parse(attribute, type)
38
+ collection = filter.query(collection)
39
+ end
25
40
  end
26
41
  collection
27
42
  end
@@ -31,6 +31,10 @@ module Headmin
31
31
  attachment.blob&.filename&.to_s
32
32
  end
33
33
 
34
+ def content_type
35
+ attachment.blob&.content_type
36
+ end
37
+
34
38
  def size
35
39
  number_to_human_size(attachment.blob&.byte_size || 0)
36
40
  end
@@ -23,6 +23,7 @@ module Headmin
23
23
  min: min,
24
24
  max: max,
25
25
  sort: sort,
26
+ accept: accept,
26
27
  required: required.nil? ? 0 : required
27
28
  }
28
29
  }).deep_merge(@wrapper || {})
@@ -45,7 +46,8 @@ module Headmin
45
46
  form: form,
46
47
  attribute: attribute,
47
48
  min: min,
48
- max: max
49
+ max: max,
50
+ accept: accept
49
51
  }
50
52
  end
51
53
 
@@ -117,6 +119,8 @@ module Headmin
117
119
  end
118
120
  end
119
121
 
122
+ attr_reader :accept
123
+
120
124
  def required
121
125
  @required ? 1 : nil
122
126
  end
@@ -16,7 +16,7 @@
16
16
  action = local_assigns.has_key?(:url) ? url : request.path
17
17
 
18
18
  begin
19
- model = controller_name.singularize.capitalize.constantize
19
+ model = controller_name.classify.constantize
20
20
  rescue
21
21
  raise "Cannot find class!"
22
22
  end
@@ -0,0 +1,23 @@
1
+ <%
2
+ # headmin/filters/field
3
+ #
4
+ # ==== Required parameters
5
+ # * +form+ - Form object
6
+ # * +attribute+ - Name of the attribute to be filtered
7
+ #
8
+ # ==== Optional parameters
9
+ # * +label+ - Display label
10
+ # * +name+ - Name of the filter parameter
11
+ #
12
+ # ==== Examples
13
+ # Basic version
14
+ # <%= render "headmin/filters", url: admin_orders_path do |form| %#>
15
+ # <%= render "headmin/filters/field", form: form, attribute: :title %#>
16
+ # <% end %#>
17
+
18
+ text = Headmin::Filter::FieldView.new(local_assigns.merge(params: params))
19
+ %>
20
+
21
+ <%= render "headmin/filters/base", text.base_options do |value| %>
22
+ <%= render "headmin/forms/text", text.input_options.merge({value: value}) %>
23
+ <% end %>
@@ -31,16 +31,14 @@
31
31
  <%= render "headmin/forms/wrapper", media.wrapper_options do %>
32
32
  <%= render "headmin/forms/label", media.label_options if media.prepend_label? %>
33
33
  <div class="h-form-file-thumbnails" data-media-target="thumbnails">
34
- <%= render "headmin/forms/media/validation", media.custom_validation_options %>
35
-
36
34
  <!-- Render previews for attachments -->
37
35
  <%= form.fields_for(media.nested_attribute, media.association_object) do |ff| %>
38
- <%= render "headmin/forms/media/item", media.item_options.merge(form: ff, url: headmin_media_url(name: media.name, ids: media.blob_ids, min: media.min, max: media.max)) %>
36
+ <%= render "headmin/forms/media/item", media.item_options.merge(form: ff, url: headmin_media_url(name: media.name, ids: media.blob_ids, min: media.min, max: media.max, mimetype: media.accept)) %>
39
37
  <% end %>
40
38
 
41
39
  <!-- Placeholder -->
42
40
  <div class="<%= "d-none" if media.attachments.any? %>" data-media-target="placeholder">
43
- <a href="<%= headmin_media_url(name: media.name, ids: media.blob_ids, min: media.min, max: media.max) %>" data-turbo-frame="remote_modal" data-media-target="modalButton">
41
+ <a href="<%= headmin_media_url(name: media.name, ids: media.blob_ids, min: media.min, max: media.max, mimetype: media.accept) %>" data-turbo-frame="remote_modal" data-media-target="modalButton">
44
42
  <%= render "headmin/thumbnail", media.thumbnail_options %>
45
43
  </a>
46
44
  </div>
@@ -50,7 +48,7 @@
50
48
  <% association_object = ActiveStorage::Attachment.new %>
51
49
  <template data-media-target="template" data-template-id-regex="<%= association_object.object_id %>">
52
50
  <%= form.fields_for(media.nested_attribute, ActiveStorage::Attachment.new, child_index: association_object.object_id) do |ff| %>
53
- <%= render "headmin/forms/media/item", media.item_options.merge(form: ff, url: headmin_media_url(name: media.name, ids: media.blob_ids, min: media.min, max: media.max)) %>
51
+ <%= render "headmin/forms/media/item", media.item_options.merge(form: ff, url: headmin_media_url(name: media.name, ids: media.blob_ids, min: media.min, max: media.max, mimetype: media.accept)) %>
54
52
  <% end %>
55
53
  </template>
56
54
 
@@ -22,7 +22,7 @@
22
22
  <%= form.label :files, class: "btn h-btn-outline-light" do %>
23
23
  <%= bootstrap_icon("upload") %>
24
24
  <%= t(".upload") %>
25
- <%= form.file_field :files, class: "d-none", multiple: true, data: {action: "change->media-modal#submitForm"} %>
25
+ <%= form.file_field :files, accept: mimetypes, class: "d-none", multiple: true, data: {action: "change->media-modal#submitForm"} %>
26
26
  <% end %>
27
27
  <% end %>
28
28
  <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><%= t(".close") %></button>
@@ -1,3 +1,3 @@
1
1
  <%= turbo_frame_tag "remote_modal" do %>
2
- <%= render "headmin/media/modal", blobs: @blobs, name: params[:name], min: params[:min], max: params[:max] %>
2
+ <%= render "headmin/media/modal", blobs: @blobs, mimetypes: @mimetypes, name: params[:name], min: params[:min], max: params[:max] %>
3
3
  <% end %>
@@ -0,0 +1,3 @@
1
+ Rails.configuration.to_prepare do
2
+ ActiveStorage::Blob.include Headmin::Blob
3
+ end
@@ -18,14 +18,6 @@ en:
18
18
  remove:
19
19
  title: Delete
20
20
  confirm: Are you sure you want to delete this?
21
- media:
22
- validation:
23
- min:
24
- one: "Please select at least 1 item"
25
- other: "Please select at least %{count} items"
26
- max:
27
- one: "Please limit your selection to maximum 1 item"
28
- other: "Please limit your selection to maximum %{count} items"
29
21
  select:
30
22
  blank: Make a choice
31
23
  repeater:
@@ -17,14 +17,6 @@ nl:
17
17
  remove:
18
18
  title: Verwijderen
19
19
  confirm: Ben je zeker dat je dit wil verwijderen?
20
- media:
21
- validation:
22
- min:
23
- one: "Gelieve minstens 1 item te selecteren"
24
- other: "Gelieve minstens %{count} items te selecteren"
25
- max:
26
- one: "Gelieve maximum 1 item te selecteren"
27
- other: "Gelieve maximum %{count} items te selecteren"
28
20
  select:
29
21
  blank: Maak een keuze
30
22
  repeater:
@@ -1,3 +1,3 @@
1
1
  module Headmin
2
- VERSION = "0.5.4"
2
+ VERSION = "0.5.6"
3
3
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "headmin",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Admin component library",
5
5
  "module": "app/assets/javascripts/headmin.js",
6
6
  "main": "app/assets/javascripts/headmin.js",
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: headmin
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.4
4
+ version: 0.5.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jef Vlamings
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-08-22 00:00:00.000000000 Z
11
+ date: 2022-11-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: closure_tree
@@ -244,6 +244,7 @@ files:
244
244
  - app/helpers/headmin/form_helper.rb
245
245
  - app/helpers/headmin/notification_helper.rb
246
246
  - app/helpers/headmin/request_helper.rb
247
+ - app/models/concerns/headmin/blob.rb
247
248
  - app/models/concerns/headmin/block.rb
248
249
  - app/models/concerns/headmin/blockable.rb
249
250
  - app/models/concerns/headmin/field.rb
@@ -268,6 +269,9 @@ files:
268
269
  - app/models/headmin/filter/conditional_view.rb
269
270
  - app/models/headmin/filter/date.rb
270
271
  - app/models/headmin/filter/date_view.rb
272
+ - app/models/headmin/filter/field.rb
273
+ - app/models/headmin/filter/field_view.rb
274
+ - app/models/headmin/filter/filter_view.rb
271
275
  - app/models/headmin/filter/flatpickr_view.rb
272
276
  - app/models/headmin/filter/menu_item_view.rb
273
277
  - app/models/headmin/filter/money.rb
@@ -336,6 +340,7 @@ files:
336
340
  - app/views/headmin/filters/_base.html.erb
337
341
  - app/views/headmin/filters/_boolean.html.erb
338
342
  - app/views/headmin/filters/_date.html.erb
343
+ - app/views/headmin/filters/_field.html.erb
339
344
  - app/views/headmin/filters/_flatpickr.html.erb
340
345
  - app/views/headmin/filters/_number.html.erb
341
346
  - app/views/headmin/filters/_options.html.erb
@@ -389,7 +394,6 @@ files:
389
394
  - app/views/headmin/forms/fields/_list.html.erb
390
395
  - app/views/headmin/forms/fields/_text.html.erb
391
396
  - app/views/headmin/forms/media/_item.html.erb
392
- - app/views/headmin/forms/media/_validation.html.erb
393
397
  - app/views/headmin/forms/repeater/_row.html.erb
394
398
  - app/views/headmin/heading/_title.html.erb
395
399
  - app/views/headmin/layout/_body.html.erb
@@ -462,6 +466,7 @@ files:
462
466
  - bin/setup
463
467
  - config/importmap.rb
464
468
  - config/initializers/customize_input_error.rb
469
+ - config/initializers/extend_active_storage_blob.rb
465
470
  - config/locales/activerecord/en.yml
466
471
  - config/locales/activerecord/nl.yml
467
472
  - config/locales/defaults/en.yml
@@ -1,10 +0,0 @@
1
- <!-- Custom validation field -->
2
- <%= form.text_field :"validation_#{attribute}",
3
- name: nil,
4
- value: nil,
5
- class: "h-form-media-validation",
6
- data: {
7
- "media-target": "validationInput",
8
- "min-message": t(".min", count: min),
9
- "max-message": t(".max", count: max),
10
- } %>