headmin 0.6.1 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
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 %>