headmin 0.6.1 → 0.6.2

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/assets/javascripts/headmin/controllers/filter_controller.js +15 -3
  4. data/app/assets/javascripts/headmin/controllers/filter_row_controller.js +75 -47
  5. data/app/assets/javascripts/headmin/controllers/infinite_scroller_controller.js +30 -0
  6. data/app/assets/javascripts/headmin/controllers/media_controller.js +24 -8
  7. data/app/assets/javascripts/headmin/controllers/media_modal_controller.js +142 -105
  8. data/app/assets/javascripts/headmin/index.js +2 -0
  9. data/app/assets/javascripts/headmin.js +122 -19
  10. data/app/assets/stylesheets/headmin.css +1 -1
  11. data/app/controllers/headmin/media_controller.rb +11 -0
  12. data/app/helpers/headmin/form_helper.rb +0 -11
  13. data/app/models/headmin/filter/association.rb +0 -12
  14. data/app/models/headmin/filter/association_count.rb +50 -0
  15. data/app/models/headmin/filter/association_count_view.rb +78 -0
  16. data/app/models/headmin/filter/base.rb +10 -0
  17. data/app/models/headmin/filter/date.rb +8 -1
  18. data/app/models/headmin/filter/date_view.rb +1 -1
  19. data/app/models/headmin/filter/number.rb +0 -12
  20. data/app/models/headmin/filter/operator_view.rb +3 -1
  21. data/app/views/headmin/_filters.html.erb +1 -1
  22. data/app/views/headmin/filters/_association_count.html.erb +28 -0
  23. data/app/views/headmin/filters/_date.html.erb +1 -0
  24. data/app/views/headmin/media/_modal.html.erb +8 -3
  25. data/app/views/headmin/media/_thumbnail.html.erb +20 -0
  26. data/app/views/headmin/media/create.turbo_stream.erb +1 -1
  27. data/app/views/headmin/media/index.turbo_stream.erb +11 -0
  28. data/app/views/headmin/media/thumbnail.html.erb +3 -0
  29. data/app/views/headmin/pagination/_infinite.html.erb +7 -0
  30. data/config/locales/headmin/filters/en.yml +2 -0
  31. data/config/locales/headmin/filters/nl.yml +2 -0
  32. data/config/locales/headmin/pagination/en.yml +2 -0
  33. data/config/locales/headmin/pagination/nl.yml +2 -0
  34. data/config/routes.rb +2 -1
  35. data/lib/headmin/version.rb +1 -1
  36. data/package.json +1 -1
  37. metadata +10 -3
  38. data/app/views/headmin/media/_item.html.erb +0 -16
@@ -7718,7 +7718,6 @@ var filter_controller_default = class extends Controller {
7718
7718
  }
7719
7719
  connect() {
7720
7720
  this.element.controller = this;
7721
- this.updateHiddenValue();
7722
7721
  }
7723
7722
  toggle(event) {
7724
7723
  const expanded = this.buttonTarget.getAttribute("aria-expanded") === "true";
@@ -7776,7 +7775,14 @@ var filter_controller_default = class extends Controller {
7776
7775
  for (const row of this.rowTargets) {
7777
7776
  const conditional = row.previousElementSibling ? row.previousElementSibling.querySelector('[data-filter-target="conditional"]').value : null;
7778
7777
  const operator = row.querySelector('[data-filter-target="operator"]').value;
7779
- const value = row.querySelector('[data-filter-target="value"]').value;
7778
+ let values = Array.from(row.querySelectorAll('[data-filter-target="value"]'));
7779
+ values = values.filter((element) => {
7780
+ return element.style.display;
7781
+ });
7782
+ values = values.map((element) => {
7783
+ return element.value;
7784
+ });
7785
+ const value = values.join(",");
7780
7786
  string += `${conditional || ""}${operator}:${value}`;
7781
7787
  }
7782
7788
  return string;
@@ -7799,16 +7805,25 @@ var filter_row_controller_default = class extends Controller {
7799
7805
  handleOperatorChange() {
7800
7806
  if (this.operatorTarget.value === "is_null" || this.operatorTarget.value === "is_not_null") {
7801
7807
  this.toggleNullInput();
7808
+ } else if (this.operatorTarget.value === "between" || this.operatorTarget.value === "not_between") {
7809
+ this.toggleSecondaryInput();
7802
7810
  } else {
7803
7811
  this.toggleOriginalInput();
7804
7812
  }
7805
7813
  }
7806
7814
  toggleNullInput() {
7807
7815
  this.hideOriginal();
7816
+ this.hideSecondary();
7808
7817
  this.showNull();
7809
7818
  }
7810
7819
  toggleOriginalInput() {
7811
7820
  this.showOriginal();
7821
+ this.hideSecondary();
7822
+ this.hideNull();
7823
+ }
7824
+ toggleSecondaryInput() {
7825
+ this.showSecondary();
7826
+ this.hideOriginal();
7812
7827
  this.hideNull();
7813
7828
  }
7814
7829
  hideOriginal() {
@@ -7819,6 +7834,22 @@ var filter_row_controller_default = class extends Controller {
7819
7834
  this.originalTarget.style.display = "block";
7820
7835
  this.originalTarget.setAttribute("data-filter-target", "value");
7821
7836
  }
7837
+ hideSecondary() {
7838
+ for (const [index2, value] of this.originalTargets.entries()) {
7839
+ if (index2 != 0) {
7840
+ value.style.display = "none";
7841
+ value.setAttribute("data-filter-target", "value_original");
7842
+ }
7843
+ }
7844
+ }
7845
+ showSecondary() {
7846
+ for (const [index2, value] of this.originalTargets.entries()) {
7847
+ if (index2 != 0) {
7848
+ value.style.display = "block";
7849
+ value.setAttribute("data-filter-target", "value");
7850
+ }
7851
+ }
7852
+ }
7822
7853
  hideNull() {
7823
7854
  this.nullTarget.style.display = "none";
7824
7855
  this.nullTarget.setAttribute("data-filter-target", "value_null");
@@ -10093,6 +10124,29 @@ var hello_controller_default = class extends Controller {
10093
10124
  }
10094
10125
  };
10095
10126
 
10127
+ // app/assets/javascripts/headmin/controllers/infinite_scroller_controller.js
10128
+ var infinite_scroller_controller_default = class extends Controller {
10129
+ connect() {
10130
+ this.clickWhenInViewport();
10131
+ document.querySelector(".modal-body").addEventListener("scroll", () => {
10132
+ this.clickWhenInViewport();
10133
+ });
10134
+ }
10135
+ clickWhenInViewport() {
10136
+ if (!this.isLoading() && this.isInViewport()) {
10137
+ this.element.setAttribute("clicked", 1);
10138
+ this.element.click();
10139
+ }
10140
+ }
10141
+ isLoading() {
10142
+ return this.element.hasAttribute("clicked");
10143
+ }
10144
+ isInViewport() {
10145
+ const rect = this.element.getBoundingClientRect();
10146
+ return rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth);
10147
+ }
10148
+ };
10149
+
10096
10150
  // app/assets/javascripts/headmin/controllers/media_controller.js
10097
10151
  var media_controller_default = class extends Controller {
10098
10152
  static get targets() {
@@ -10140,8 +10194,8 @@ var media_controller_default = class extends Controller {
10140
10194
  this.postProcess();
10141
10195
  }
10142
10196
  selectItems(items) {
10143
- this.removeAllItems();
10144
- this.addItems(items);
10197
+ this.removeAllDeselectedItems(items);
10198
+ this.addNewItems(items);
10145
10199
  this.postProcess();
10146
10200
  }
10147
10201
  postProcess() {
@@ -10184,8 +10238,16 @@ var media_controller_default = class extends Controller {
10184
10238
  }
10185
10239
  });
10186
10240
  }
10187
- addItems(items) {
10188
- items.forEach((item) => this.addItem(item));
10241
+ addNewItems(items) {
10242
+ const itemTargetIds = this.itemTargets.map((i) => {
10243
+ return parseInt(i.querySelectorAll("input")[1].value);
10244
+ });
10245
+ items.forEach((item) => {
10246
+ if (itemTargetIds.includes(item.blobId)) {
10247
+ return;
10248
+ }
10249
+ this.addItem(item);
10250
+ });
10189
10251
  }
10190
10252
  addItem(item) {
10191
10253
  const currentItem = this.itemByBlobId(item.blobId);
@@ -10230,11 +10292,18 @@ var media_controller_default = class extends Controller {
10230
10292
  const randomNumber = Math.floor(1e8 + Math.random() * 9e8);
10231
10293
  return template.innerHTML.replace(regex, randomNumber);
10232
10294
  }
10233
- removeAllItems() {
10234
- this.removeItems(this.itemTargets);
10295
+ removeAllDeselectedItems(items) {
10296
+ this.removeDeselectedItems(items, this.itemTargets);
10235
10297
  }
10236
- removeItems(items) {
10298
+ removeDeselectedItems(elements, items) {
10299
+ const returnedBlobIds = elements.map((e) => {
10300
+ return e.blobId;
10301
+ });
10237
10302
  items.forEach((item) => {
10303
+ const blobId = parseInt(item.querySelectorAll("input")[1].value);
10304
+ if (returnedBlobIds.includes(blobId)) {
10305
+ return;
10306
+ }
10238
10307
  this.removeItem(item);
10239
10308
  });
10240
10309
  }
@@ -10267,6 +10336,9 @@ var media_modal_controller_default = class extends Controller {
10267
10336
  static get targets() {
10268
10337
  return ["idCheckbox", "item", "form", "selectButton", "placeholder", "count"];
10269
10338
  }
10339
+ static get values() {
10340
+ return { ids: Array };
10341
+ }
10270
10342
  connect() {
10271
10343
  this.validate();
10272
10344
  this.updateCount();
@@ -10278,34 +10350,64 @@ var media_modal_controller_default = class extends Controller {
10278
10350
  this.hidePlaceholder();
10279
10351
  this.triggerFormSubmission();
10280
10352
  }
10281
- inputChange() {
10282
- this.handleInputChange();
10353
+ inputChange(event) {
10354
+ this.handleIdsUpdate(event.target);
10283
10355
  this.updateCount();
10284
10356
  }
10285
10357
  hidePlaceholder() {
10286
10358
  this.placeholderTarget.classList.add("d-none");
10287
10359
  }
10288
- handleInputChange() {
10360
+ handleIdsUpdate(element) {
10361
+ if (element.checked) {
10362
+ let arr = this.idsValue;
10363
+ arr.push(element.value);
10364
+ this.idsValue = arr;
10365
+ } else {
10366
+ this.idsValue = this.idsValue.filter((value) => {
10367
+ return element.value !== value;
10368
+ });
10369
+ }
10370
+ }
10371
+ itemTargetConnected(element) {
10372
+ this.updateItem(element.querySelector("input"));
10373
+ }
10374
+ updateItem(element) {
10375
+ const arr = this.idsValue;
10376
+ if (arr.includes(element.value)) {
10377
+ element.checked = true;
10378
+ } else {
10379
+ element.checked = false;
10380
+ }
10381
+ }
10382
+ idsValueChanged() {
10383
+ for (const item of this.itemTargets) {
10384
+ this.updateItem(item.querySelector("input"));
10385
+ }
10289
10386
  this.validate();
10290
10387
  }
10291
10388
  dispatchSelectionEvent() {
10292
10389
  document.dispatchEvent(new CustomEvent("mediaSelectionSubmitted", {
10293
10390
  detail: {
10294
10391
  name: this.element.dataset.name,
10295
- items: this.renderItemsForEvent(this.selectedItems())
10392
+ items: this.renderItemsForEvent()
10296
10393
  }
10297
10394
  }));
10298
10395
  }
10299
10396
  triggerFormSubmission() {
10300
10397
  this.formTarget.requestSubmit();
10301
10398
  }
10302
- renderItemsForEvent(items) {
10303
- return items.map((item) => this.renderItemForEvent(item));
10399
+ renderItemsForEvent() {
10400
+ return this.idsValue.map((item) => this.renderItemForEvent(item)).filter((i) => {
10401
+ return i !== void 0;
10402
+ });
10304
10403
  }
10305
10404
  renderItemForEvent(item) {
10405
+ const id = parseInt(item);
10406
+ const blob_id = `#blob_${id}`;
10407
+ const element = this.element.querySelector(blob_id);
10306
10408
  return {
10307
- blobId: item.querySelector('input[type="checkbox"]').value,
10308
- thumbnail: item.querySelector(".h-thumbnail")
10409
+ blobId: id,
10410
+ thumbnail: element ? element.querySelector(".h-thumbnail") : ""
10309
10411
  };
10310
10412
  }
10311
10413
  selectedItems() {
@@ -10315,7 +10417,7 @@ var media_modal_controller_default = class extends Controller {
10315
10417
  });
10316
10418
  }
10317
10419
  selectedItemsCount() {
10318
- return this.selectedItems().length;
10420
+ return this.idsValue.length;
10319
10421
  }
10320
10422
  minSelectedItems() {
10321
10423
  return parseInt(this.element.dataset.min, 10) || 0;
@@ -10341,7 +10443,7 @@ var media_modal_controller_default = class extends Controller {
10341
10443
  return count >= this.minSelectedItems() && count <= this.maxSelectedItems();
10342
10444
  }
10343
10445
  updateCount() {
10344
- this.countTarget.innerHTML = this.idCheckboxTargets.filter((checkbox) => checkbox.checked).length;
10446
+ this.countTarget.innerHTML = this.selectedItemsCount();
10345
10447
  }
10346
10448
  };
10347
10449
 
@@ -15901,6 +16003,7 @@ var Headmin = class {
15901
16003
  Stimulus.register("filters", filters_controller_default);
15902
16004
  Stimulus.register("flatpickr", flatpickr_controller_default);
15903
16005
  Stimulus.register("hello", hello_controller_default);
16006
+ Stimulus.register("infinite-scroller", infinite_scroller_controller_default);
15904
16007
  Stimulus.register("media", media_controller_default);
15905
16008
  Stimulus.register("media-modal", media_modal_controller_default);
15906
16009
  Stimulus.register("notification", notification_controller_default);
@@ -1,7 +1,7 @@
1
1
  @charset "UTF-8";
2
2
  @import "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css";
3
3
 
4
- /* sass-plugin-0:/opt/homebrew/var/www/headmin/src/scss/headmin.scss */
4
+ /* sass-plugin-0:/usr/local/var/www/headmin/src/scss/headmin.scss */
5
5
  :root {
6
6
  --bs-blue: #0d6efd;
7
7
  --bs-indigo: #6610f2;
@@ -1,4 +1,5 @@
1
1
  class Headmin::MediaController < HeadminController
2
+ include Headmin::Pagination
2
3
  layout false
3
4
 
4
5
  def index
@@ -9,7 +10,13 @@ class Headmin::MediaController < HeadminController
9
10
  .order(created_at: :desc)
10
11
  .group(:id)
11
12
  .all
13
+ @blobs = paginate(@blobs)
12
14
  @mimetypes = media_params[:mimetype]
15
+
16
+ respond_to do |format|
17
+ format.html
18
+ format.turbo_stream
19
+ end
13
20
  end
14
21
 
15
22
  def create
@@ -38,6 +45,10 @@ class Headmin::MediaController < HeadminController
38
45
  end
39
46
  end
40
47
 
48
+ def thumbnail
49
+ @blob = ActiveStorage::Blob.find(params[:id])
50
+ end
51
+
41
52
  private
42
53
 
43
54
  def media_params
@@ -1,16 +1,5 @@
1
1
  module Headmin
2
2
  module FormHelper
3
- # TODO: cleanup after input field refactoring
4
- def form_field_validation_id(form, name)
5
- [form.object_name, name.to_s, "validation"].join("_").parameterize.underscore
6
- end
7
-
8
- # TODO: cleanup after input field refactoring
9
- def form_field_validation_class(form, name)
10
- return nil if request.get?
11
- form.object.errors.has_key?(name) ? "is-invalid" : "is-valid"
12
- end
13
-
14
3
  # Outputs currently present query parameters as hidden fields for a given form
15
4
  #
16
5
  # https://example.com/products?amount=1&type[]=food&type[]=beverage
@@ -69,18 +69,6 @@ module Headmin
69
69
  def has_many?
70
70
  macro == :has_many
71
71
  end
72
-
73
- private
74
-
75
- def is_i?(value)
76
- # Regex: this selects signed digits (\d) only, it is then checked to the value, e.g.:
77
- # is_i?("3") = true
78
- # is_i?("-3") = true
79
- # is_i?("3a") = false
80
- # is_i?("3.2") = false
81
-
82
- /\A[-+]?\d+\z/.match(value)
83
- end
84
72
  end
85
73
  end
86
74
  end
@@ -0,0 +1,50 @@
1
+ module Headmin
2
+ module Filter
3
+ class AssociationCount < Headmin::Filter::Base
4
+ OPERATORS = %w[eq not_eq gt gteq lt lteq]
5
+
6
+ def cast_value(value)
7
+ is_i?(value) ? value.to_i : 0
8
+ end
9
+
10
+ def query(collection)
11
+ return collection unless @instructions.any?
12
+
13
+ # Store the collections' class for later use
14
+ @parent_class = collection.is_a?(Class) ? collection : collection.klass
15
+
16
+ # Join table and group on primary key if necessary
17
+ collection = collection.left_joins(reflection.name).group(primary_key)
18
+
19
+ # Build query and execute
20
+ query = nil
21
+ @instructions.each do |instruction|
22
+ query = build_query(query, collection, instruction)
23
+ end
24
+ collection.having(query)
25
+ end
26
+
27
+ private
28
+
29
+ def build_query(query, collection, instruction)
30
+ query_operator = convert_to_query_operator(instruction[:operator])
31
+ query_value = convert_to_query_value(instruction[:value], instruction[:operator])
32
+
33
+ query_operator, query_value = process_null_operators(query_operator, query_value)
34
+ new_query = reflection.klass.arel_table[reflection.foreign_key.to_sym].count.send(query_operator, query_value)
35
+ query ? query.send(instruction[:conditional], new_query) : new_query
36
+ end
37
+
38
+ def reflection
39
+ reflection = @parent_class.reflect_on_association(attribute.to_s.split("_")[0].to_sym)
40
+ raise UnknownAssociation if reflection.nil?
41
+
42
+ reflection
43
+ end
44
+
45
+ def primary_key
46
+ "#{@parent_class.table_name}.#{@parent_class.primary_key}"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,78 @@
1
+ module Headmin
2
+ module Filter
3
+ class AssociationCountView < 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
+ def collection
17
+ @collection || association_model.all.map { |record| [record.to_s, record.id] }
18
+ end
19
+
20
+ def association_model
21
+ reflection.klass
22
+ end
23
+
24
+ private
25
+
26
+ def id
27
+ "#{name}_value"
28
+ end
29
+
30
+ def name
31
+ @name || attribute
32
+ end
33
+
34
+ def attribute
35
+ "#{@association}_count"
36
+ end
37
+
38
+ def label
39
+ @label || I18n.t("attributes.#{attribute}", default: "#{I18n.t("attributes.count")} #{association_model.model_name.human(count: collection? ? 2 : 1)}")
40
+ end
41
+
42
+ def reflection
43
+ form.object.class.reflect_on_association(@association)
44
+ end
45
+
46
+ def collection?
47
+ reflection.collection?
48
+ end
49
+
50
+ def default_base_options
51
+ {
52
+ label: label,
53
+ name: attribute,
54
+ display_values: collection,
55
+ filter: Headmin::Filter::AssociationCount.new(name, @params),
56
+ allowed_operators: Headmin::Filter::AssociationCount::OPERATORS
57
+ }
58
+ end
59
+
60
+ def default_input_options
61
+ {
62
+ label: false,
63
+ wrapper: false,
64
+ name: nil,
65
+ id: id,
66
+ data: {
67
+ action: "change->filter#updateHiddenValue",
68
+ filter_target: "value",
69
+ filter_row_target: "original"
70
+ },
71
+ collection: collection,
72
+ selected: selected,
73
+ class: "form-control"
74
+ }
75
+ end
76
+ end
77
+ end
78
+ end
@@ -100,6 +100,16 @@ module Headmin
100
100
  query ? query.send(instruction[:conditional], new_query) : new_query
101
101
  end
102
102
 
103
+ def is_i?(value)
104
+ # Regex: this selects signed digits (\d) only, it is then checked to the value, e.g.:
105
+ # is_i?("3") = true
106
+ # is_i?("-3") = true
107
+ # is_i?("3a") = false
108
+ # is_i?("3.2") = false
109
+
110
+ /\A[-+]?\d+\z/.match(value)
111
+ end
112
+
103
113
  private
104
114
 
105
115
  def parse(string)
@@ -56,8 +56,15 @@ module Headmin
56
56
  def display_value(value)
57
57
  # This uses the default date format of headmin.
58
58
  # Can be overwritten by setting default date format of the application.
59
- if value.class.to_s == "Range"
59
+
60
+ current_operator = instructions.find { |instruction| instruction[:value] == value }[:operator]
61
+
62
+ # To make the operators eq and not_eq work, we pass a range.
63
+ # However, display value should return this as a date and not a range.
64
+ if value.class.to_s == "Range" && (current_operator == "eq" || current_operator == "not_eq")
60
65
  I18n.l(value.last.to_date)
66
+ elsif values.class.to_s
67
+ "#{I18n.l(value.first.to_date)} - #{I18n.l(value.last.to_date)}"
61
68
  else
62
69
  I18n.l(value.to_date)
63
70
  end
@@ -28,7 +28,7 @@ module Headmin
28
28
  label: label,
29
29
  name: attribute,
30
30
  filter: Headmin::Filter::Date.new(name, @params),
31
- allowed_operators: Headmin::Filter::Date::OPERATORS - %w[in not_in between not_between]
31
+ allowed_operators: Headmin::Filter::Date::OPERATORS - %w[in not_in]
32
32
  }
33
33
  end
34
34
 
@@ -10,18 +10,6 @@ module Headmin
10
10
  def to_s
11
11
  string
12
12
  end
13
-
14
- private
15
-
16
- def is_i?(value)
17
- # Regex: this selects signed digits (\d) only, it is then checked to the value, e.g.:
18
- # is_i?("3") = true
19
- # is_i?("-3") = true
20
- # is_i?("3a") = false
21
- # is_i?("3.2") = false
22
-
23
- /\A[-+]?\d+\z/.match(value)
24
- end
25
13
  end
26
14
  end
27
15
  end
@@ -24,7 +24,9 @@ module Headmin
24
24
  is_null: "&#9675; #{I18n.t("headmin.filters.operators.is_null")}",
25
25
  is_not_null: "&#9679; #{I18n.t("headmin.filters.operators.is_not_null")}",
26
26
  in: "&ni; #{I18n.t("headmin.filters.operators.in")}",
27
- not_in: "&notni; #{I18n.t("headmin.filters.operators.not_in")}"
27
+ not_in: "&notni; #{I18n.t("headmin.filters.operators.not_in")}",
28
+ between: "&harr; #{I18n.t("headmin.filters.operators.between")}",
29
+ not_between: "&harrcir; #{I18n.t("headmin.filters.operators.not_between")}"
28
30
  }
29
31
  end
30
32
  end
@@ -23,7 +23,7 @@
23
23
 
24
24
  <!-- Default parameters (e.g. sorting, pagination) -->
25
25
  <% default_params.except("page").each do |name, value| %>
26
- <%= form.hidden_field name.to_sym, value: value %>
26
+ <%= hidden_field_tag(name, value) %>
27
27
  <% end %>
28
28
 
29
29
  <div class="d-flex flex-column flex-md-row align-content-start align-items-md-start">
@@ -0,0 +1,28 @@
1
+ <%
2
+ # headmin/filters/association_count
3
+ #
4
+ # ==== Required parameters
5
+ # * +association+ - Name of the association that has to be counted
6
+ # * +form+ - Form object
7
+ #
8
+ # ==== Optional parameters
9
+ # * +label+ - Display label
10
+ # * +name+ - Name of the filter parameter
11
+ #
12
+ # ==== Examples
13
+ # Basic version (one-to-many)
14
+ # <%= render "headmin/filters", url: admin_orders_path do |form| %#>
15
+ # <%= render "headmin/filters/association_count", form: form, association: :beverages %#>
16
+ # <% end %#>
17
+ #
18
+ # Basic version (one-to-one)
19
+ # <%= render "headmin/filters", url: admin_orders_path do |form| %#>
20
+ # <%= render "headmin/filters/association_count", form: form, association: :beverage %#>
21
+ # <% end %#>
22
+
23
+ number = Headmin::Filter::AssociationCountView.new(local_assigns.merge(params: params))
24
+ %>
25
+
26
+ <%= render "headmin/filters/base", number.base_options do |value| %>
27
+ <%= render "headmin/forms/number", number.input_options.merge({value: value}) %>
28
+ <% end %>
@@ -20,4 +20,5 @@
20
20
 
21
21
  <%= render "headmin/filters/base", date.base_options do |value| %>
22
22
  <%= render "headmin/forms/date", date.input_options.merge({value: value}) %>
23
+ <%= render "headmin/forms/date_range", date.input_options.merge({start: {value: value.class.to_s == "Range" ? value.first : value}, end: {value: value.class.to_s == "Range" ? value.last : value}}) %>
23
24
  <% end %>
@@ -1,4 +1,4 @@
1
- <div class="media-modal modal fade" tabindex="-1" data-controller="remote-modal media-modal" data-name="<%= name %>" data-min="<%= min %>" data-max="<%= max %>">
1
+ <div class="media-modal modal fade" tabindex="-1" data-controller="remote-modal media-modal" data-media-modal-ids-value="<%= params[:ids] ? params[:ids] : [] %>" data-name="<%= name %>" data-min="<%= min %>" data-max="<%= max %>">
2
2
  <div class="modal-dialog modal-lg modal-dialog-scrollable">
3
3
  <div class="modal-content">
4
4
  <div class="modal-header">
@@ -10,15 +10,20 @@
10
10
  <div class="modal-body">
11
11
  <%= turbo_frame_tag "thumbnails", class: "d-flex flex-wrap gap-2" do %>
12
12
  <% @blobs.each do |blob| %>
13
- <%= render "headmin/media/item", blob: blob %>
13
+ <%= turbo_frame_tag blob, src: headmin_media_item_thumbnail_path(blob), loading: "lazy" do %>
14
+ <%= render "thumbnail" %>
15
+ <% end %>
14
16
  <% end %>
15
17
  <div data-media-modal-target="placeholder" class="<%= "d-none" if !@blobs.empty? %>">
16
18
  <p><%= t(".placeholder") %></p>
17
19
  </div>
18
20
  <% end %>
21
+ <div class="mt-3">
22
+ <%= render "headmin/pagination/infinite", items: @blobs %>
23
+ </div>
19
24
  </div>
20
25
  <div class="modal-footer">
21
- <%= form_with url: headmin_media_path, multipart: true, data: {"media-modal-target": "form"}, class: "me-auto" do |form| %>
26
+ <%= form_with url: headmin_new_media_path, multipart: true, data: {"media-modal-target": "form"}, class: "me-auto" do |form| %>
22
27
  <%= form.label :files, class: "btn h-btn-outline-light" do %>
23
28
  <%= bootstrap_icon("upload") %>
24
29
  <%= t(".upload") %>
@@ -0,0 +1,20 @@
1
+ <% blob = local_assigns.has_key?(:blob) ? local_assigns[:blob] : nil %>
2
+ <% if blob.present? %>
3
+ <div data-media-modal-target="item" title="<%= "#{blob.filename} (#{l(blob.created_at, format: :long)})" %>">
4
+ <!-- Input -->
5
+ <input
6
+ id="media-item-<%= blob.id %>"
7
+ type="checkbox"
8
+ value="<%= blob.id %>"
9
+ data-action="change->media-modal#inputChange"
10
+ data-media-modal-target="idCheckbox"
11
+ hidden>
12
+
13
+ <!-- Label -->
14
+ <label for="media-item-<%= blob.id %>">
15
+ <%= render "headmin/thumbnail", file: blob %>
16
+ </label>
17
+ </div>
18
+ <% else %>
19
+ <%= render "headmin/thumbnail", file: nil %>
20
+ <% end %>
@@ -1,5 +1,5 @@
1
1
  <%= turbo_stream.prepend "thumbnails" do %>
2
2
  <% @blobs.each do |blob| %>
3
- <%= render "headmin/media/item", blob: blob %>
3
+ <%= render "headmin/media/thumbnail", blob: blob %>
4
4
  <% end %>
5
5
  <% end %>
@@ -0,0 +1,11 @@
1
+ <%= turbo_stream.append "thumbnails" do %>
2
+ <% @blobs.each do |blob| %>
3
+ <%= turbo_frame_tag blob, src: headmin_media_item_thumbnail_path(blob), loading: "lazy" do %>
4
+ <%= render 'thumbnail' %>
5
+ <% end %>
6
+ <% end %>
7
+ <% end %>
8
+
9
+ <%= turbo_stream.replace "infinite" do %>
10
+ <%= render "headmin/pagination/infinite", items: @blobs %>
11
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= turbo_frame_tag @blob do %>
2
+ <%= render "thumbnail", blob: @blob %>
3
+ <% end %>