headmin 0.5.1 → 0.5.4

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 (136) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2 -2
  3. data/Gemfile +14 -0
  4. data/Gemfile.lock +79 -2
  5. data/app/assets/javascripts/headmin/controllers/media_controller.js +237 -0
  6. data/app/assets/javascripts/headmin/controllers/media_modal_controller.js +110 -0
  7. data/app/assets/javascripts/headmin/controllers/remote_modal_controller.js +9 -0
  8. data/app/assets/javascripts/headmin/controllers/textarea_controller.js +34 -0
  9. data/app/assets/javascripts/headmin/index.js +8 -0
  10. data/app/assets/javascripts/headmin.js +294 -0
  11. data/app/assets/stylesheets/headmin/forms/file.scss +40 -5
  12. data/app/assets/stylesheets/headmin/forms/media.scss +10 -0
  13. data/app/assets/stylesheets/headmin/forms.scss +1 -0
  14. data/app/assets/stylesheets/headmin/layout/sidebar.scss +0 -1
  15. data/app/assets/stylesheets/headmin/media/index.scss +9 -0
  16. data/app/assets/stylesheets/headmin/media.scss +1 -0
  17. data/app/assets/stylesheets/headmin/overrides/redactorx.scss +1 -1
  18. data/app/assets/stylesheets/headmin/table.scss +8 -0
  19. data/app/assets/stylesheets/headmin/vendor/{tom-select-bootstrap.css → tom-select-bootstrap.scss} +0 -1
  20. data/app/assets/stylesheets/headmin.css +61 -8
  21. data/app/assets/stylesheets/headmin.scss +1 -0
  22. data/app/controllers/headmin/media_controller.rb +52 -0
  23. data/app/controllers/headmin_controller.rb +2 -0
  24. data/app/helpers/headmin/form_helper.rb +2 -2
  25. data/app/models/concerns/headmin/field.rb +2 -2
  26. data/app/models/concerns/headmin/fieldable.rb +19 -10
  27. data/app/models/concerns/headmin/form/hintable.rb +6 -1
  28. data/app/models/headmin/filter/association.rb +86 -0
  29. data/app/models/headmin/filter/association_view.rb +74 -0
  30. data/app/models/headmin/filter/base.rb +5 -2
  31. data/app/models/headmin/filter/boolean_view.rb +1 -0
  32. data/app/models/headmin/filter/date.rb +49 -1
  33. data/app/models/headmin/filter/date_view.rb +1 -0
  34. data/app/models/headmin/filter/flatpickr_view.rb +1 -0
  35. data/app/models/headmin/filter/number_view.rb +1 -0
  36. data/app/models/headmin/filter/operator_view.rb +3 -1
  37. data/app/models/headmin/filter/options_view.rb +1 -0
  38. data/app/models/headmin/filter/text_view.rb +1 -0
  39. data/app/models/headmin/form/association_view.rb +102 -0
  40. data/app/models/headmin/form/blocks_view.rb +4 -1
  41. data/app/models/headmin/form/file_view.rb +0 -8
  42. data/app/models/headmin/form/flatpickr_view.rb +2 -1
  43. data/app/models/headmin/form/media_item_view.rb +39 -0
  44. data/app/models/headmin/form/media_view.rb +137 -0
  45. data/app/models/headmin/form/select_view.rb +2 -1
  46. data/app/models/headmin/form/textarea_view.rb +6 -1
  47. data/app/models/headmin/thumbnail_view.rb +40 -19
  48. data/app/models/view_model.rb +4 -0
  49. data/app/views/examples/admin.html.erb +8 -8
  50. data/app/views/examples/auth.html.erb +2 -2
  51. data/app/views/headmin/_breadcrumbs.html.erb +2 -2
  52. data/app/views/headmin/_dropdown.html.erb +1 -1
  53. data/app/views/headmin/_filters.html.erb +12 -7
  54. data/app/views/headmin/_pagination.html.erb +2 -2
  55. data/app/views/headmin/_popup.html.erb +4 -4
  56. data/app/views/headmin/_table.html.erb +1 -1
  57. data/app/views/headmin/_thumbnail.html.erb +33 -9
  58. data/app/views/headmin/dropdown/_button.html.erb +2 -2
  59. data/app/views/headmin/dropdown/_item.html.erb +2 -2
  60. data/app/views/headmin/dropdown/_list.html.erb +3 -3
  61. data/app/views/headmin/dropdown/_locale.html.erb +5 -5
  62. data/app/views/headmin/filters/_association.html.erb +24 -0
  63. data/app/views/headmin/filters/_options.html.erb +1 -1
  64. data/app/views/headmin/filters/filter/_button.html.erb +2 -2
  65. data/app/views/headmin/filters/filter/_null_select.html.erb +2 -2
  66. data/app/views/headmin/forms/_association.html.erb +30 -0
  67. data/app/views/headmin/forms/_errors.html.erb +1 -1
  68. data/app/views/headmin/forms/_file.html.erb +10 -11
  69. data/app/views/headmin/forms/_hint.html.erb +6 -1
  70. data/app/views/headmin/forms/_media.html.erb +60 -0
  71. data/app/views/headmin/forms/_repeater.html.erb +18 -16
  72. data/app/views/headmin/forms/_textarea.html.erb +1 -1
  73. data/app/views/headmin/forms/_wrapper.html.erb +0 -1
  74. data/app/views/headmin/forms/fields/_list.html.erb +6 -4
  75. data/app/views/headmin/forms/media/_item.html.erb +38 -0
  76. data/app/views/headmin/forms/media/_validation.html.erb +10 -0
  77. data/app/views/headmin/forms/repeater/_row.html.erb +17 -15
  78. data/app/views/headmin/heading/_title.html.erb +2 -2
  79. data/app/views/headmin/layout/_main.html.erb +2 -0
  80. data/app/views/headmin/layout/_remote_modal.html.erb +1 -0
  81. data/app/views/headmin/layout/_sidebar.html.erb +1 -1
  82. data/app/views/headmin/media/_item.html.erb +16 -0
  83. data/app/views/headmin/media/_media_item_modal.html.erb +51 -0
  84. data/app/views/headmin/media/_modal.html.erb +35 -0
  85. data/app/views/headmin/media/create.turbo_stream.erb +5 -0
  86. data/app/views/headmin/media/index.html.erb +3 -0
  87. data/app/views/headmin/media/show.html.erb +9 -0
  88. data/app/views/headmin/media/update.turbo_stream.erb +3 -0
  89. data/app/views/headmin/nav/_dropdown.html.erb +7 -7
  90. data/app/views/headmin/nav/_item.html.erb +5 -5
  91. data/app/views/headmin/nav/item/_locale.html.erb +6 -6
  92. data/app/views/headmin/pagination/_per_page.html.erb +7 -7
  93. data/app/views/headmin/pagination/kaminari/_first_page.html.erb +2 -2
  94. data/app/views/headmin/pagination/kaminari/_gap.html.erb +1 -1
  95. data/app/views/headmin/pagination/kaminari/_last_page.html.erb +2 -2
  96. data/app/views/headmin/pagination/kaminari/_next_page.html.erb +3 -3
  97. data/app/views/headmin/pagination/kaminari/_page.html.erb +2 -2
  98. data/app/views/headmin/pagination/kaminari/_paginator.html.erb +1 -1
  99. data/app/views/headmin/pagination/kaminari/_prev_page.html.erb +2 -2
  100. data/app/views/headmin/table/_actions.html.erb +9 -9
  101. data/app/views/headmin/table/_body.html.erb +1 -1
  102. data/app/views/headmin/table/actions/_action.html.erb +4 -4
  103. data/app/views/headmin/table/actions/_export.html.erb +1 -1
  104. data/app/views/headmin/table/body/_association.html.erb +17 -3
  105. data/app/views/headmin/table/body/_boolean.erb +4 -4
  106. data/app/views/headmin/table/body/_date.html.erb +2 -2
  107. data/app/views/headmin/table/body/_image.html.erb +18 -0
  108. data/app/views/headmin/table/body/_string.html.erb +1 -1
  109. data/app/views/headmin/table/head/_cell.html.erb +1 -1
  110. data/app/views/headmin/table/head/cell/_asc.html.erb +2 -2
  111. data/app/views/headmin/table/head/cell/_default.html.erb +1 -1
  112. data/app/views/headmin/table/head/cell/_desc.html.erb +1 -1
  113. data/app/views/headmin/views/devise/confirmations/_new.html.erb +1 -1
  114. data/app/views/headmin/views/devise/passwords/_edit.html.erb +1 -1
  115. data/app/views/headmin/views/devise/passwords/_new.html.erb +1 -1
  116. data/app/views/headmin/views/devise/registrations/_edit.html.erb +5 -5
  117. data/app/views/headmin/views/devise/registrations/_new.html.erb +1 -1
  118. data/app/views/headmin/views/devise/sessions/_new.html.erb +1 -1
  119. data/app/views/headmin/views/devise/shared/_links.html.erb +14 -20
  120. data/app/views/headmin/views/devise/unlocks/_new.html.erb +1 -1
  121. data/config/locales/activerecord/en.yml +1 -0
  122. data/config/locales/activerecord/nl.yml +1 -0
  123. data/config/locales/devise/nl.yml +1 -1
  124. data/config/locales/headmin/filters/en.yml +3 -1
  125. data/config/locales/headmin/filters/nl.yml +2 -0
  126. data/config/locales/headmin/forms/en.yml +8 -0
  127. data/config/locales/headmin/forms/nl.yml +8 -0
  128. data/config/locales/headmin/media/en.yml +23 -0
  129. data/config/locales/headmin/media/nl.yml +23 -0
  130. data/config/locales/headmin/table/en.yml +2 -0
  131. data/config/locales/headmin/table/nl.yml +2 -0
  132. data/config/routes.rb +10 -0
  133. data/lib/generators/templates/views/layouts/auth.html.erb +2 -2
  134. data/lib/headmin/version.rb +1 -1
  135. data/package.json +1 -1
  136. metadata +34 -3
@@ -10093,6 +10093,260 @@ var hello_controller_default = class extends Controller {
10093
10093
  }
10094
10094
  };
10095
10095
 
10096
+ // app/assets/javascripts/headmin/controllers/media_controller.js
10097
+ var media_controller_default = class extends Controller {
10098
+ static get targets() {
10099
+ return ["item", "template", "thumbnails", "modalButton", "placeholder", "validationInput", "count", "editButton"];
10100
+ }
10101
+ connect() {
10102
+ document.addEventListener("mediaSelectionSubmitted", (event) => {
10103
+ if (event.detail.name === this.element.dataset.name) {
10104
+ this.selectItems(event.detail.items);
10105
+ }
10106
+ });
10107
+ if (this.hasSorting()) {
10108
+ this.initSortable();
10109
+ }
10110
+ this.validate();
10111
+ }
10112
+ destroy(event) {
10113
+ const item = event.currentTarget.closest("[data-media-target='item']");
10114
+ this.destroyItem(item);
10115
+ }
10116
+ syncIds() {
10117
+ const ids = this.activeIds();
10118
+ this.modalButtonTargets.forEach((button) => {
10119
+ const url = new URL(button.getAttribute("href"));
10120
+ url.searchParams.delete("ids[]");
10121
+ ids.forEach((id) => {
10122
+ url.searchParams.append("ids[]", id);
10123
+ });
10124
+ button.setAttribute("href", url.toString());
10125
+ });
10126
+ }
10127
+ initSortable() {
10128
+ sortable_esm_default.create(this.thumbnailsTarget, {
10129
+ handle: ".media-drag-sort-handle",
10130
+ onEnd: (event) => {
10131
+ this.resetPositions();
10132
+ }
10133
+ });
10134
+ }
10135
+ hasSorting() {
10136
+ return this.element.dataset.sort === "true";
10137
+ }
10138
+ destroyItem(item) {
10139
+ this.removeItem(item);
10140
+ this.postProcess();
10141
+ }
10142
+ selectItems(items) {
10143
+ this.removeAllItems();
10144
+ this.addItems(items);
10145
+ this.postProcess();
10146
+ }
10147
+ postProcess() {
10148
+ this.resetPositions();
10149
+ this.syncIds();
10150
+ 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
+ }
10181
+ resetPositions() {
10182
+ this.activeItems().forEach((item, index2) => {
10183
+ const positionInput = item.querySelector("input[name*='position']");
10184
+ if (positionInput) {
10185
+ positionInput.value = index2;
10186
+ }
10187
+ });
10188
+ }
10189
+ addItems(items) {
10190
+ items.forEach((item) => this.addItem(item));
10191
+ }
10192
+ addItem(item) {
10193
+ const currentItem = this.itemByBlobId(item.blobId);
10194
+ if (currentItem) {
10195
+ this.enableItem(currentItem);
10196
+ } else {
10197
+ this.createItem(item);
10198
+ }
10199
+ }
10200
+ togglePlaceholder() {
10201
+ if (this.activeItems().length > 0) {
10202
+ this.hidePlaceholder();
10203
+ } else {
10204
+ this.showPlaceholder();
10205
+ }
10206
+ }
10207
+ showPlaceholder() {
10208
+ this.placeholderTarget.classList.remove("d-none");
10209
+ }
10210
+ hidePlaceholder() {
10211
+ this.placeholderTarget.classList.add("d-none");
10212
+ }
10213
+ enableItem(item) {
10214
+ item.querySelector("input[name*='_destroy']").value = false;
10215
+ item.classList.remove("d-none");
10216
+ }
10217
+ createItem(item) {
10218
+ const template = this.templateTarget;
10219
+ const html = this.randomizeIds(template);
10220
+ this.thumbnailsTarget.insertAdjacentHTML("beforeend", html);
10221
+ const newItem = this.itemTargets.pop();
10222
+ newItem.querySelector('input[name*="[blob_id]"]').value = item.blobId;
10223
+ newItem.querySelector('input[name*="[_destroy]"]').value = false;
10224
+ const editButton = newItem.querySelector('[data-media-target="editButton"]');
10225
+ editButton.setAttribute("href", editButton.getAttribute("href").replace("$1", item.blobId));
10226
+ const oldThumbnail = newItem.querySelector(".h-thumbnail");
10227
+ const newThumbnail = item.thumbnail.cloneNode(true);
10228
+ oldThumbnail.parentNode.replaceChild(newThumbnail, oldThumbnail);
10229
+ }
10230
+ randomizeIds(template) {
10231
+ const regex = new RegExp(template.dataset.templateIdRegex, "g");
10232
+ const randomNumber = Math.floor(1e8 + Math.random() * 9e8);
10233
+ return template.innerHTML.replace(regex, randomNumber);
10234
+ }
10235
+ removeAllItems() {
10236
+ this.removeItems(this.itemTargets);
10237
+ }
10238
+ removeItems(items) {
10239
+ items.forEach((item) => {
10240
+ this.removeItem(item);
10241
+ });
10242
+ }
10243
+ removeItem(item) {
10244
+ item.querySelector("input[name*='_destroy']").value = 1;
10245
+ item.classList.add("d-none");
10246
+ this.resetPositions();
10247
+ this.syncIds();
10248
+ this.togglePlaceholder();
10249
+ }
10250
+ itemByBlobId(blobId) {
10251
+ return this.itemTargets.find((item) => {
10252
+ return item.querySelector("input[name*='blob_id']").value === blobId;
10253
+ });
10254
+ }
10255
+ activeItems() {
10256
+ return this.itemTargets.filter((item) => {
10257
+ return item.querySelector("input[name$='[_destroy]']").value === "false";
10258
+ });
10259
+ }
10260
+ activeIds() {
10261
+ return this.activeItems().map((item) => {
10262
+ return item.querySelector("input[name$='[blob_id]']").value;
10263
+ });
10264
+ }
10265
+ };
10266
+
10267
+ // app/assets/javascripts/headmin/controllers/media_modal_controller.js
10268
+ var media_modal_controller_default = class extends Controller {
10269
+ static get targets() {
10270
+ return ["idCheckbox", "item", "form", "selectButton", "placeholder", "count"];
10271
+ }
10272
+ connect() {
10273
+ this.validate();
10274
+ this.updateCount();
10275
+ }
10276
+ select() {
10277
+ this.dispatchSelectionEvent();
10278
+ }
10279
+ submitForm() {
10280
+ this.hidePlaceholder();
10281
+ this.triggerFormSubmission();
10282
+ }
10283
+ inputChange() {
10284
+ this.handleInputChange();
10285
+ this.updateCount();
10286
+ }
10287
+ hidePlaceholder() {
10288
+ this.placeholderTarget.classList.add("d-none");
10289
+ }
10290
+ handleInputChange() {
10291
+ this.validate();
10292
+ }
10293
+ dispatchSelectionEvent() {
10294
+ document.dispatchEvent(new CustomEvent("mediaSelectionSubmitted", {
10295
+ detail: {
10296
+ name: this.element.dataset.name,
10297
+ items: this.renderItemsForEvent(this.selectedItems())
10298
+ }
10299
+ }));
10300
+ }
10301
+ triggerFormSubmission() {
10302
+ this.formTarget.requestSubmit();
10303
+ }
10304
+ renderItemsForEvent(items) {
10305
+ return items.map((item) => this.renderItemForEvent(item));
10306
+ }
10307
+ renderItemForEvent(item) {
10308
+ return {
10309
+ blobId: item.querySelector('input[type="checkbox"]').value,
10310
+ thumbnail: item.querySelector(".h-thumbnail")
10311
+ };
10312
+ }
10313
+ selectedItems() {
10314
+ return this.itemTargets.filter((item) => {
10315
+ const checkbox = item.querySelector('input[type="checkbox"]');
10316
+ return checkbox.checked;
10317
+ });
10318
+ }
10319
+ selectedItemsCount() {
10320
+ return this.selectedItems().length;
10321
+ }
10322
+ minSelectedItems() {
10323
+ return parseInt(this.element.dataset.min, 10) || 0;
10324
+ }
10325
+ maxSelectedItems() {
10326
+ return parseInt(this.element.dataset.max, 10) || Infinity;
10327
+ }
10328
+ validate() {
10329
+ if (this.isValid()) {
10330
+ this.enableSelectButton();
10331
+ } else {
10332
+ this.disableSelectButton();
10333
+ }
10334
+ }
10335
+ enableSelectButton() {
10336
+ this.selectButtonTarget.removeAttribute("disabled");
10337
+ }
10338
+ disableSelectButton() {
10339
+ this.selectButtonTarget.setAttribute("disabled", "");
10340
+ }
10341
+ isValid() {
10342
+ const count = this.selectedItemsCount();
10343
+ return count >= this.minSelectedItems() && count <= this.maxSelectedItems();
10344
+ }
10345
+ updateCount() {
10346
+ this.countTarget.innerHTML = this.idCheckboxTargets.filter((checkbox) => checkbox.checked).length;
10347
+ }
10348
+ };
10349
+
10096
10350
  // node_modules/@popperjs/core/lib/index.js
10097
10351
  var lib_exports = {};
10098
10352
  __export(lib_exports, {
@@ -15236,6 +15490,14 @@ var redactorx_controller_default = class extends Controller {
15236
15490
  }
15237
15491
  };
15238
15492
 
15493
+ // app/assets/javascripts/headmin/controllers/remote_modal_controller.js
15494
+ var remote_modal_controller_default = class extends Controller {
15495
+ connect() {
15496
+ this.modal = new Modal(this.element);
15497
+ this.modal.show();
15498
+ }
15499
+ };
15500
+
15239
15501
  // app/assets/javascripts/headmin/controllers/repeater_controller.js
15240
15502
  var repeater_controller_default = class extends Controller {
15241
15503
  static get values() {
@@ -15580,6 +15842,34 @@ var table_controller_default = class extends Controller {
15580
15842
  }
15581
15843
  };
15582
15844
 
15845
+ // app/assets/javascripts/headmin/controllers/textarea_controller.js
15846
+ var textarea_controller_default = class extends Controller {
15847
+ static get targets() {
15848
+ return ["textarea", "count"];
15849
+ }
15850
+ connect() {
15851
+ this.update();
15852
+ }
15853
+ update() {
15854
+ this.resize();
15855
+ this.updateCount();
15856
+ }
15857
+ resize() {
15858
+ this.textareaTarget.style.height = "auto";
15859
+ this.textareaTarget.setAttribute("style", "height:" + this.textareaTarget.scrollHeight + "px;overflow-y:hidden;");
15860
+ }
15861
+ updateCount() {
15862
+ if (this.textareaTarget.getAttribute("maxlength")) {
15863
+ this.updateCountLength();
15864
+ }
15865
+ }
15866
+ updateCountLength() {
15867
+ const currentLength = this.textareaTarget.value.length;
15868
+ const maximumLength = this.textareaTarget.getAttribute("maxlength");
15869
+ this.countTarget.textContent = `${currentLength}/${maximumLength}`;
15870
+ }
15871
+ };
15872
+
15583
15873
  // app/assets/javascripts/headmin/index.js
15584
15874
  var Headmin = class {
15585
15875
  static start() {
@@ -15594,13 +15884,17 @@ var Headmin = class {
15594
15884
  Stimulus.register("filters", filters_controller_default);
15595
15885
  Stimulus.register("flatpickr", flatpickr_controller_default);
15596
15886
  Stimulus.register("hello", hello_controller_default);
15887
+ Stimulus.register("media", media_controller_default);
15888
+ Stimulus.register("media-modal", media_modal_controller_default);
15597
15889
  Stimulus.register("notification", notification_controller_default);
15598
15890
  Stimulus.register("popup", popup_controller_default);
15599
15891
  Stimulus.register("redactorx", redactorx_controller_default);
15892
+ Stimulus.register("remote-modal", remote_modal_controller_default);
15600
15893
  Stimulus.register("repeater", repeater_controller_default);
15601
15894
  Stimulus.register("select", select_controller_default);
15602
15895
  Stimulus.register("table", table_controller_default);
15603
15896
  Stimulus.register("table-actions", table_actions_controller_default);
15897
+ Stimulus.register("textarea", textarea_controller_default);
15604
15898
  }
15605
15899
  };
15606
15900
  export {
@@ -17,26 +17,41 @@
17
17
  display: flex;
18
18
  flex-wrap: wrap;
19
19
  gap: map-get($spacers, 2);
20
+
21
+ a, a:link, a:hover {
22
+ color: inherit;
23
+ text-decoration: inherit;
24
+ }
20
25
  }
21
26
 
22
27
  .h-form-file-thumbnail {
23
28
  position: relative;
24
29
  pointer-events: initial !important;
25
30
  display: flex;
31
+
32
+ &:hover {
33
+ .h-form-file-thumbnail-actions {
34
+ display: block;
35
+ }
36
+ }
37
+ }
38
+
39
+ .h-form-file-thumbnail-actions {
40
+ display: none;
26
41
  }
27
42
 
28
43
  .h-form-file-thumbnail-remove {
29
44
  position: absolute;
30
- top: 0;
31
- right: 0;
45
+ top: 5px;
46
+ right: 5px;
32
47
  background: $danger;
33
- width: 20px;
34
- height: 20px;
48
+ width: 25px;
49
+ height: 25px;
35
50
  display: flex;
36
51
  align-items: center;
37
52
  justify-content: center;
38
53
  color: white;
39
- border-radius: $border-radius;
54
+ border-radius: $border-radius-pill;
40
55
  z-index: 3;
41
56
  cursor: pointer;
42
57
 
@@ -44,3 +59,23 @@
44
59
  background: tint-color($danger, $btn-hover-bg-tint-amount)
45
60
  }
46
61
  }
62
+
63
+ .h-form-file-thumbnail-edit {
64
+ position: absolute;
65
+ top: 5px;
66
+ right: 35px;
67
+ background-color: rgba(0, 0, 0, 0.7);
68
+ width: 25px;
69
+ height: 25px;
70
+ display: flex;
71
+ align-items: center;
72
+ justify-content: center;
73
+ border-radius: $border-radius-pill;
74
+ z-index: 3;
75
+ cursor: pointer;
76
+ color: white !important;
77
+
78
+ &:hover {
79
+ background-color: rgba(0, 0, 0, 1);
80
+ }
81
+ }
@@ -0,0 +1,10 @@
1
+ .h-form-media-validation {
2
+ opacity: 0;
3
+ width: 100px;
4
+ height: 0;
5
+ padding: 0;
6
+ margin: 0;
7
+ position: absolute;
8
+ bottom: 0;
9
+ left: 0;
10
+ }
@@ -7,5 +7,6 @@
7
7
 
8
8
  @import "forms/autocomplete";
9
9
  @import "forms/file";
10
+ @import "forms/media";
10
11
  @import "forms/repeater";
11
12
  @import "forms/search";
@@ -14,7 +14,6 @@
14
14
  .nav-brand {
15
15
  svg, img {
16
16
  max-width: 100%;
17
- height: 30px;
18
17
  }
19
18
  svg {
20
19
  fill: $white;
@@ -0,0 +1,9 @@
1
+ .media-modal {
2
+ .h-thumbnail {
3
+ cursor: pointer;
4
+ }
5
+
6
+ input[type="checkbox"]:checked + label .h-thumbnail {
7
+ background-color: #0d6efd;
8
+ }
9
+ }
@@ -0,0 +1 @@
1
+ @import "media/index";
@@ -40,7 +40,7 @@
40
40
  padding: $input-padding-y $input-padding-x !important;
41
41
  position: relative;
42
42
 
43
- p {
43
+ p, ul li, ol li, td {
44
44
  @include font-size($input-font-size);
45
45
  line-height: $input-line-height;
46
46
  color: $input-color;
@@ -35,6 +35,14 @@
35
35
  }
36
36
  }
37
37
 
38
+ .h-table-cell-image {
39
+ padding: 0.4rem 1rem !important;
40
+
41
+ img {
42
+ border-radius: 3px;
43
+ }
44
+ }
45
+
38
46
  .h-table-cell-color {
39
47
  width: 22px;
40
48
  height: 22px;
@@ -516,7 +516,6 @@
516
516
  .ts-wrapper.form-select {
517
517
  padding: 0;
518
518
  height: auto;
519
- box-shadow: none;
520
519
  }
521
520
  .ts-wrapper.form-select .ts-control, .ts-wrapper.form-select.single.input-active .ts-control {
522
521
  border: none !important;
@@ -10180,7 +10180,6 @@ fieldset:disabled .btn {
10180
10180
  .ts-wrapper.form-select {
10181
10181
  padding: 0;
10182
10182
  height: auto;
10183
- box-shadow: none;
10184
10183
  }
10185
10184
  .ts-wrapper.form-select .ts-control,
10186
10185
  .ts-wrapper.form-select.single.input-active .ts-control {
@@ -12944,7 +12943,10 @@ span.flatpickr-weekday {
12944
12943
  padding: 0.375rem 0.75rem !important;
12945
12944
  position: relative;
12946
12945
  }
12947
- .rx-content p {
12946
+ .rx-content p,
12947
+ .rx-content ul li,
12948
+ .rx-content ol li,
12949
+ .rx-content td {
12948
12950
  font-size: 0.9rem;
12949
12951
  line-height: 1.5;
12950
12952
  color: #212529;
@@ -13019,29 +13021,69 @@ span.flatpickr-weekday {
13019
13021
  flex-wrap: wrap;
13020
13022
  gap: 0.5rem;
13021
13023
  }
13024
+ .h-form-file-thumbnails a,
13025
+ .h-form-file-thumbnails a:link,
13026
+ .h-form-file-thumbnails a:hover {
13027
+ color: inherit;
13028
+ text-decoration: inherit;
13029
+ }
13022
13030
  .h-form-file-thumbnail {
13023
13031
  position: relative;
13024
13032
  pointer-events: initial !important;
13025
13033
  display: flex;
13026
13034
  }
13035
+ .h-form-file-thumbnail:hover .h-form-file-thumbnail-actions {
13036
+ display: block;
13037
+ }
13038
+ .h-form-file-thumbnail-actions {
13039
+ display: none;
13040
+ }
13027
13041
  .h-form-file-thumbnail-remove {
13028
13042
  position: absolute;
13029
- top: 0;
13030
- right: 0;
13043
+ top: 5px;
13044
+ right: 5px;
13031
13045
  background: #dc3545;
13032
- width: 20px;
13033
- height: 20px;
13046
+ width: 25px;
13047
+ height: 25px;
13034
13048
  display: flex;
13035
13049
  align-items: center;
13036
13050
  justify-content: center;
13037
13051
  color: white;
13038
- border-radius: 0.25rem;
13052
+ border-radius: 50rem;
13039
13053
  z-index: 3;
13040
13054
  cursor: pointer;
13041
13055
  }
13042
13056
  .h-form-file-thumbnail-remove:hover {
13043
13057
  background: #e15361;
13044
13058
  }
13059
+ .h-form-file-thumbnail-edit {
13060
+ position: absolute;
13061
+ top: 5px;
13062
+ right: 35px;
13063
+ background-color: rgba(0, 0, 0, 0.7);
13064
+ width: 25px;
13065
+ height: 25px;
13066
+ display: flex;
13067
+ align-items: center;
13068
+ justify-content: center;
13069
+ border-radius: 50rem;
13070
+ z-index: 3;
13071
+ cursor: pointer;
13072
+ color: white !important;
13073
+ }
13074
+ .h-form-file-thumbnail-edit:hover {
13075
+ background-color: black;
13076
+ }
13077
+ .h-form-media-validation {
13078
+ opacity: 0;
13079
+ width: 100px;
13080
+ height: 0;
13081
+ padding: 0;
13082
+ margin: 0;
13083
+ position: absolute;
13084
+ bottom: 0;
13085
+ left: 0;
13086
+ }
13045
13087
  .repeater {
13046
13088
  position: relative;
13047
13089
  }
@@ -13138,7 +13180,6 @@ mark,
13138
13180
  .nav-brand svg,
13139
13181
  .nav-brand img {
13140
13182
  max-width: 100%;
13141
- height: 30px;
13142
13183
  }
13143
13184
  .nav-brand svg {
13144
13185
  fill: #ffffff;
@@ -13204,6 +13245,12 @@ body.empty {
13204
13245
  width: 300px;
13205
13246
  max-width: 100%;
13206
13247
  }
13248
+ .h-table-cell-image {
13249
+ padding: 0.4rem 1rem !important;
13250
+ }
13251
+ .h-table-cell-image img {
13252
+ border-radius: 3px;
13253
+ }
13207
13254
  .h-table-cell-color {
13208
13255
  width: 22px;
13209
13256
  height: 22px;
@@ -13390,6 +13437,12 @@ body.empty {
13390
13437
  z-index: 9;
13391
13438
  display: none;
13392
13439
  }
13440
+ .media-modal .h-thumbnail {
13441
+ cursor: pointer;
13442
+ }
13443
+ .media-modal input[type=checkbox]:checked + label .h-thumbnail {
13444
+ background-color: #0d6efd;
13445
+ }
13393
13446
  .h-popup {
13394
13447
  padding: 10px;
13395
13448
  background: #ffffff;
@@ -60,6 +60,7 @@
60
60
  @import "headmin/table";
61
61
  @import "headmin/utilities";
62
62
  @import "headmin/filter";
63
+ @import "headmin/media";
63
64
  @import "headmin/popup";
64
65
  @import "headmin/thumbnail";
65
66
  @import "headmin/syntax";
@@ -0,0 +1,52 @@
1
+ class Headmin::MediaController < HeadminController
2
+ layout false
3
+
4
+ def index
5
+ @blobs =
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
10
+ .order(created_at: :desc)
11
+ .group(:id)
12
+ .all
13
+ end
14
+
15
+ def create
16
+ blobs = []
17
+ media_params[:files].reject { |c| c.blank? }.each do |file|
18
+ blobs << ActiveStorage::Blob.create_and_upload!(io: file, filename: file.original_filename)
19
+ end
20
+
21
+ respond_to do |format|
22
+ format.turbo_stream {
23
+ @blobs = blobs
24
+ }
25
+ format.html { redirect_to root_path }
26
+ end
27
+ end
28
+
29
+ def show
30
+ @blob = ActiveStorage::Blob.find(params[:id])
31
+ end
32
+
33
+ def update
34
+ @blob = ActiveStorage::Blob.find(params[:id])
35
+ media_item_params[:filename] = media_item_params[:filename] + "." + @blob.filename.to_s.rpartition(".").last
36
+ if @blob.update(media_item_params)
37
+ flash.now[:notice] = t("admin.flash.updated", name: @blob.filename)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def media_params
44
+ params.permit(
45
+ files: []
46
+ )
47
+ end
48
+
49
+ def media_item_params
50
+ params.require(:blob).permit!
51
+ end
52
+ end
@@ -0,0 +1,2 @@
1
+ class HeadminController < ApplicationController
2
+ end
@@ -16,8 +16,8 @@ module Headmin
16
16
  # https://example.com/products?amount=1&type[]=food&type[]=beverage
17
17
  #
18
18
  # <%= form.hidden_input :amount, value: 1 %>
19
- # <%= form.hidden_input :'type[]', value: 'food' %>
20
- # <%= form.hidden_input :'type[]', value: 'beverage' %>
19
+ # <%= form.hidden_input :"type[]", value: "food" %>
20
+ # <%= form.hidden_input :"type[]", value: "beverage" %>
21
21
  def query_parameter_fields(form)
22
22
  test = request.query_parameters.map do |name, value|
23
23
  if value.is_a?(Array)
@@ -4,7 +4,7 @@ module Headmin
4
4
 
5
5
  included do
6
6
  # Configuration
7
- has_closure_tree
7
+ has_closure_tree order: "position", numeric_order: true
8
8
 
9
9
  # Associations
10
10
  belongs_to :fieldable, polymorphic: true, optional: true, touch: true
@@ -13,7 +13,7 @@ module Headmin
13
13
  accepts_nested_attributes_for :fields, allow_destroy: true
14
14
 
15
15
  # field_type: :files, :file
16
- has_many_attached :files
16
+ has_many_attached :files, dependent: :detach
17
17
  accepts_nested_attributes_for :files_attachments, allow_destroy: true
18
18
  end
19
19
  end