headmin 0.6.1 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +5 -4
- data/app/assets/javascripts/headmin/controllers/filter_controller.js +15 -3
- data/app/assets/javascripts/headmin/controllers/filter_row_controller.js +28 -0
- data/app/assets/javascripts/headmin/controllers/infinite_scroller_controller.js +28 -0
- data/app/assets/javascripts/headmin/controllers/media_controller.js +24 -8
- data/app/assets/javascripts/headmin/controllers/media_modal_controller.js +47 -10
- data/app/assets/javascripts/headmin/index.js +2 -0
- data/app/assets/javascripts/headmin.js +122 -19
- data/app/controllers/headmin/media_controller.rb +11 -0
- data/app/helpers/headmin/form_helper.rb +0 -11
- data/app/models/headmin/filter/association.rb +0 -12
- data/app/models/headmin/filter/association_count.rb +50 -0
- data/app/models/headmin/filter/association_count_view.rb +78 -0
- data/app/models/headmin/filter/base.rb +10 -0
- data/app/models/headmin/filter/date.rb +8 -1
- data/app/models/headmin/filter/date_view.rb +1 -1
- data/app/models/headmin/filter/number.rb +0 -12
- data/app/models/headmin/filter/operator_view.rb +3 -1
- data/app/views/headmin/_filters.html.erb +1 -1
- data/app/views/headmin/filters/_association_count.html.erb +28 -0
- data/app/views/headmin/filters/_date.html.erb +1 -0
- data/app/views/headmin/media/_modal.html.erb +8 -3
- data/app/views/headmin/media/_thumbnail.html.erb +20 -0
- data/app/views/headmin/media/create.turbo_stream.erb +3 -1
- data/app/views/headmin/media/index.turbo_stream.erb +11 -0
- data/app/views/headmin/media/thumbnail.html.erb +3 -0
- data/app/views/headmin/pagination/_infinite.html.erb +7 -0
- data/config/locales/headmin/filters/en.yml +2 -0
- data/config/locales/headmin/filters/nl.yml +2 -0
- data/config/locales/headmin/pagination/en.yml +2 -0
- data/config/locales/headmin/pagination/nl.yml +2 -0
- data/config/routes.rb +2 -1
- data/lib/headmin/version.rb +1 -1
- data/package.json +1 -1
- metadata +11 -4
- data/app/views/headmin/media/_item.html.erb +0 -16
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 819c038f09a0d2dab83f4931d67256dd6130244afcff409af0580d8abf72d02c
         | 
| 4 | 
            +
              data.tar.gz: 28fa2501518683ed07ce67315a42b62b1f2d8749208196a84ef20642125b815b
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: a117642e4cd9cc87484aa37ca85c0ba95a0e0747111b51ce87c529c92510a4b7dfa715aa432d8993b37b412aab30fa51f4a0b4876e2058d2fd83086ef1145a54
         | 
| 7 | 
            +
              data.tar.gz: 06247200375b3fa43820d017fd7a174e29e5acea8a8c4fda239c95a8388e6df33918e1995fa4488e77c8c915ea8f9fc715d50f390e11edffc60794ca6eeeb2e3
         | 
    
        data/Gemfile.lock
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            PATH
         | 
| 2 2 | 
             
              remote: .
         | 
| 3 3 | 
             
              specs:
         | 
| 4 | 
            -
                headmin (0.6. | 
| 4 | 
            +
                headmin (0.6.3)
         | 
| 5 5 | 
             
                  closure_tree (~> 7.4)
         | 
| 6 6 | 
             
                  inline_svg (~> 1.7)
         | 
| 7 7 | 
             
                  redcarpet (~> 3.5)
         | 
| @@ -218,7 +218,7 @@ GEM | |
| 218 218 | 
             
                  zeitwerk (~> 2.5)
         | 
| 219 219 | 
             
                rainbow (3.1.1)
         | 
| 220 220 | 
             
                rake (13.0.6)
         | 
| 221 | 
            -
                redcarpet (3. | 
| 221 | 
            +
                redcarpet (3.6.0)
         | 
| 222 222 | 
             
                regexp_parser (2.6.2)
         | 
| 223 223 | 
             
                reline (0.3.2)
         | 
| 224 224 | 
             
                  io-console (~> 0.5)
         | 
| @@ -288,8 +288,9 @@ GEM | |
| 288 288 | 
             
                websocket-driver (0.7.5)
         | 
| 289 289 | 
             
                  websocket-extensions (>= 0.1.0)
         | 
| 290 290 | 
             
                websocket-extensions (0.1.5)
         | 
| 291 | 
            -
                with_advisory_lock ( | 
| 292 | 
            -
                  activerecord (>=  | 
| 291 | 
            +
                with_advisory_lock (5.1.0)
         | 
| 292 | 
            +
                  activerecord (>= 6.1)
         | 
| 293 | 
            +
                  zeitwerk (>= 2.6)
         | 
| 293 294 | 
             
                xpath (3.2.0)
         | 
| 294 295 | 
             
                  nokogiri (~> 1.8)
         | 
| 295 296 | 
             
                zeitwerk (2.6.6)
         | 
| @@ -15,8 +15,6 @@ export default class extends Controller { | |
| 15 15 | 
             
              // This allows calling controller methods from the element in other controllers
         | 
| 16 16 | 
             
              connect () {
         | 
| 17 17 | 
             
                this.element.controller = this
         | 
| 18 | 
            -
             | 
| 19 | 
            -
                this.updateHiddenValue()
         | 
| 20 18 | 
             
              }
         | 
| 21 19 |  | 
| 22 20 | 
             
              toggle (event) {
         | 
| @@ -89,7 +87,21 @@ export default class extends Controller { | |
| 89 87 | 
             
                for (const row of this.rowTargets) {
         | 
| 90 88 | 
             
                  const conditional = row.previousElementSibling ? row.previousElementSibling.querySelector('[data-filter-target="conditional"]').value : null
         | 
| 91 89 | 
             
                  const operator = row.querySelector('[data-filter-target="operator"]').value
         | 
| 92 | 
            -
                   | 
| 90 | 
            +
                  let values = Array.from(row.querySelectorAll('[data-filter-target="value"]'))
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                  // Only the visible elements are of interest
         | 
| 93 | 
            +
                  values = values.filter((element) => {
         | 
| 94 | 
            +
                    return element.style.display
         | 
| 95 | 
            +
                  })
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  // Grab the value of each visible element
         | 
| 98 | 
            +
                  values = values.map((element) => {
         | 
| 99 | 
            +
                    return element.value
         | 
| 100 | 
            +
                  })
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                  // Concatenate array to a string
         | 
| 103 | 
            +
                  const value = values.join(',')
         | 
| 104 | 
            +
             | 
| 93 105 | 
             
                  string += `${conditional || ''}${operator}:${value}`
         | 
| 94 106 | 
             
                }
         | 
| 95 107 |  | 
| @@ -13,6 +13,8 @@ export default class extends Controller { | |
| 13 13 | 
             
              handleOperatorChange () {
         | 
| 14 14 | 
             
                if (this.operatorTarget.value === 'is_null' || this.operatorTarget.value === 'is_not_null') {
         | 
| 15 15 | 
             
                  this.toggleNullInput()
         | 
| 16 | 
            +
                } else if (this.operatorTarget.value === 'between' || this.operatorTarget.value === 'not_between') {
         | 
| 17 | 
            +
                  this.toggleSecondaryInput()
         | 
| 16 18 | 
             
                } else {
         | 
| 17 19 | 
             
                  this.toggleOriginalInput()
         | 
| 18 20 | 
             
                }
         | 
| @@ -20,11 +22,19 @@ export default class extends Controller { | |
| 20 22 |  | 
| 21 23 | 
             
              toggleNullInput () {
         | 
| 22 24 | 
             
                this.hideOriginal()
         | 
| 25 | 
            +
                this.hideSecondary()
         | 
| 23 26 | 
             
                this.showNull()
         | 
| 24 27 | 
             
              }
         | 
| 25 28 |  | 
| 26 29 | 
             
              toggleOriginalInput () {
         | 
| 27 30 | 
             
                this.showOriginal()
         | 
| 31 | 
            +
                this.hideSecondary()
         | 
| 32 | 
            +
                this.hideNull()
         | 
| 33 | 
            +
              }
         | 
| 34 | 
            +
             | 
| 35 | 
            +
              toggleSecondaryInput () {
         | 
| 36 | 
            +
                this.showSecondary()
         | 
| 37 | 
            +
                this.hideOriginal()
         | 
| 28 38 | 
             
                this.hideNull()
         | 
| 29 39 | 
             
              }
         | 
| 30 40 |  | 
| @@ -38,6 +48,24 @@ export default class extends Controller { | |
| 38 48 | 
             
                this.originalTarget.setAttribute('data-filter-target', 'value')
         | 
| 39 49 | 
             
              }
         | 
| 40 50 |  | 
| 51 | 
            +
              hideSecondary () {
         | 
| 52 | 
            +
                for (const [index, value] of this.originalTargets.entries()) {
         | 
| 53 | 
            +
                  if (index !== 0) {
         | 
| 54 | 
            +
                    value.style.display = 'none'
         | 
| 55 | 
            +
                    value.setAttribute('data-filter-target', 'value_original')
         | 
| 56 | 
            +
                  }
         | 
| 57 | 
            +
                }
         | 
| 58 | 
            +
              }
         | 
| 59 | 
            +
             | 
| 60 | 
            +
              showSecondary () {
         | 
| 61 | 
            +
                for (const [index, value] of this.originalTargets.entries()) {
         | 
| 62 | 
            +
                  if (index !== 0) {
         | 
| 63 | 
            +
                    value.style.display = 'block'
         | 
| 64 | 
            +
                    value.setAttribute('data-filter-target', 'value')
         | 
| 65 | 
            +
                  }
         | 
| 66 | 
            +
                }
         | 
| 67 | 
            +
              }
         | 
| 68 | 
            +
             | 
| 41 69 | 
             
              hideNull () {
         | 
| 42 70 | 
             
                this.nullTarget.style.display = 'none'
         | 
| 43 71 | 
             
                this.nullTarget.setAttribute('data-filter-target', 'value_null')
         | 
| @@ -0,0 +1,28 @@ | |
| 1 | 
            +
            import { Controller } from '@hotwired/stimulus'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            export default class extends Controller {
         | 
| 4 | 
            +
              connect () {
         | 
| 5 | 
            +
                this.clickWhenInViewport()
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                document.querySelector('.modal-body').addEventListener('scroll', () => {
         | 
| 8 | 
            +
                  this.clickWhenInViewport()
         | 
| 9 | 
            +
                })
         | 
| 10 | 
            +
              }
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              clickWhenInViewport () {
         | 
| 13 | 
            +
                if (!this.isLoading() && this.isInViewport()) {
         | 
| 14 | 
            +
                  this.element.setAttribute('clicked', 1)
         | 
| 15 | 
            +
                  this.element.click()
         | 
| 16 | 
            +
                }
         | 
| 17 | 
            +
              }
         | 
| 18 | 
            +
             | 
| 19 | 
            +
              isLoading () {
         | 
| 20 | 
            +
                return this.element.hasAttribute('clicked')
         | 
| 21 | 
            +
              }
         | 
| 22 | 
            +
             | 
| 23 | 
            +
              isInViewport () {
         | 
| 24 | 
            +
                const rect = this.element.getBoundingClientRect()
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                return rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth)
         | 
| 27 | 
            +
              }
         | 
| 28 | 
            +
            }
         | 
| @@ -66,10 +66,10 @@ export default class extends Controller { | |
| 66 66 |  | 
| 67 67 | 
             
              selectItems (items) {
         | 
| 68 68 | 
             
                // Destroy all deselected items
         | 
| 69 | 
            -
                this. | 
| 69 | 
            +
                this.removeAllDeselectedItems(items)
         | 
| 70 70 |  | 
| 71 | 
            -
                // Add all selected items
         | 
| 72 | 
            -
                this. | 
| 71 | 
            +
                // Add all new selected items
         | 
| 72 | 
            +
                this.addNewItems(items)
         | 
| 73 73 |  | 
| 74 74 | 
             
                this.postProcess()
         | 
| 75 75 | 
             
              }
         | 
| @@ -129,8 +129,16 @@ export default class extends Controller { | |
| 129 129 | 
             
                })
         | 
| 130 130 | 
             
              }
         | 
| 131 131 |  | 
| 132 | 
            -
               | 
| 133 | 
            -
                 | 
| 132 | 
            +
              addNewItems (items) {
         | 
| 133 | 
            +
                const itemTargetIds = this.itemTargets.map((i) => { return parseInt(i.querySelectorAll('input')[1].value) })
         | 
| 134 | 
            +
                items.forEach((item) => {
         | 
| 135 | 
            +
                  if (itemTargetIds.includes(item.blobId)) {
         | 
| 136 | 
            +
                    // Do not add this item (as it is already present)
         | 
| 137 | 
            +
                    return
         | 
| 138 | 
            +
                  }
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                  this.addItem(item)
         | 
| 141 | 
            +
                })
         | 
| 134 142 | 
             
              }
         | 
| 135 143 |  | 
| 136 144 | 
             
              addItem (item) {
         | 
| @@ -192,12 +200,20 @@ export default class extends Controller { | |
| 192 200 | 
             
                return template.innerHTML.replace(regex, randomNumber)
         | 
| 193 201 | 
             
              }
         | 
| 194 202 |  | 
| 195 | 
            -
               | 
| 196 | 
            -
                this. | 
| 203 | 
            +
              removeAllDeselectedItems (items) {
         | 
| 204 | 
            +
                this.removeDeselectedItems(items, this.itemTargets)
         | 
| 197 205 | 
             
              }
         | 
| 198 206 |  | 
| 199 | 
            -
               | 
| 207 | 
            +
              removeDeselectedItems (elements, items) {
         | 
| 208 | 
            +
                const returnedBlobIds = elements.map((e) => { return e.blobId })
         | 
| 209 | 
            +
             | 
| 200 210 | 
             
                items.forEach((item) => {
         | 
| 211 | 
            +
                  const blobId = parseInt(item.querySelectorAll('input')[1].value)
         | 
| 212 | 
            +
                  if (returnedBlobIds.includes(blobId)) {
         | 
| 213 | 
            +
                    // Do not delete this one
         | 
| 214 | 
            +
                    return
         | 
| 215 | 
            +
                  }
         | 
| 216 | 
            +
             | 
| 201 217 | 
             
                  this.removeItem(item)
         | 
| 202 218 | 
             
                })
         | 
| 203 219 | 
             
              }
         | 
| @@ -6,6 +6,10 @@ export default class extends Controller { | |
| 6 6 | 
             
                return ['idCheckbox', 'item', 'form', 'selectButton', 'placeholder', 'count']
         | 
| 7 7 | 
             
              }
         | 
| 8 8 |  | 
| 9 | 
            +
              static get values () {
         | 
| 10 | 
            +
                return { ids: Array }
         | 
| 11 | 
            +
              }
         | 
| 12 | 
            +
             | 
| 9 13 | 
             
              connect () {
         | 
| 10 14 | 
             
                this.validate()
         | 
| 11 15 | 
             
                this.updateCount()
         | 
| @@ -21,8 +25,8 @@ export default class extends Controller { | |
| 21 25 | 
             
                this.triggerFormSubmission()
         | 
| 22 26 | 
             
              }
         | 
| 23 27 |  | 
| 24 | 
            -
              inputChange () {
         | 
| 25 | 
            -
                this. | 
| 28 | 
            +
              inputChange (event) {
         | 
| 29 | 
            +
                this.handleIdsUpdate(event.target)
         | 
| 26 30 | 
             
                this.updateCount()
         | 
| 27 31 | 
             
              }
         | 
| 28 32 |  | 
| @@ -31,7 +35,36 @@ export default class extends Controller { | |
| 31 35 | 
             
                this.placeholderTarget.classList.add('d-none')
         | 
| 32 36 | 
             
              }
         | 
| 33 37 |  | 
| 34 | 
            -
               | 
| 38 | 
            +
              handleIdsUpdate (element) {
         | 
| 39 | 
            +
                if (element.checked) {
         | 
| 40 | 
            +
                  const arr = this.idsValue
         | 
| 41 | 
            +
                  arr.push(element.value)
         | 
| 42 | 
            +
                  this.idsValue = arr
         | 
| 43 | 
            +
                } else {
         | 
| 44 | 
            +
                  this.idsValue = this.idsValue.filter((value) => {
         | 
| 45 | 
            +
                    return element.value !== value
         | 
| 46 | 
            +
                  })
         | 
| 47 | 
            +
                }
         | 
| 48 | 
            +
              }
         | 
| 49 | 
            +
             | 
| 50 | 
            +
              itemTargetConnected (element) {
         | 
| 51 | 
            +
                this.updateItem(element.querySelector('input'))
         | 
| 52 | 
            +
              }
         | 
| 53 | 
            +
             | 
| 54 | 
            +
              updateItem (element) {
         | 
| 55 | 
            +
                const arr = this.idsValue
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                if (arr.includes(element.value)) {
         | 
| 58 | 
            +
                  element.checked = true
         | 
| 59 | 
            +
                } else {
         | 
| 60 | 
            +
                  element.checked = false
         | 
| 61 | 
            +
                }
         | 
| 62 | 
            +
              }
         | 
| 63 | 
            +
             | 
| 64 | 
            +
              idsValueChanged () {
         | 
| 65 | 
            +
                for (const item of this.itemTargets) {
         | 
| 66 | 
            +
                  this.updateItem(item.querySelector('input'))
         | 
| 67 | 
            +
                }
         | 
| 35 68 | 
             
                this.validate()
         | 
| 36 69 | 
             
              }
         | 
| 37 70 |  | 
| @@ -42,7 +75,7 @@ export default class extends Controller { | |
| 42 75 | 
             
                    {
         | 
| 43 76 | 
             
                      detail: {
         | 
| 44 77 | 
             
                        name: this.element.dataset.name,
         | 
| 45 | 
            -
                        items: this.renderItemsForEvent( | 
| 78 | 
            +
                        items: this.renderItemsForEvent()
         | 
| 46 79 | 
             
                      }
         | 
| 47 80 | 
             
                    }
         | 
| 48 81 | 
             
                  )
         | 
| @@ -53,14 +86,18 @@ export default class extends Controller { | |
| 53 86 | 
             
                this.formTarget.requestSubmit()
         | 
| 54 87 | 
             
              }
         | 
| 55 88 |  | 
| 56 | 
            -
              renderItemsForEvent ( | 
| 57 | 
            -
                return  | 
| 89 | 
            +
              renderItemsForEvent () {
         | 
| 90 | 
            +
                return this.idsValue.map((item) => this.renderItemForEvent(item)).filter((i) => { return i !== undefined })
         | 
| 58 91 | 
             
              }
         | 
| 59 92 |  | 
| 60 93 | 
             
              renderItemForEvent (item) {
         | 
| 94 | 
            +
                const id = parseInt(item)
         | 
| 95 | 
            +
                const blobId = `#blob_${id}`
         | 
| 96 | 
            +
                const element = this.element.querySelector(blobId)
         | 
| 97 | 
            +
             | 
| 61 98 | 
             
                return {
         | 
| 62 | 
            -
                  blobId:  | 
| 63 | 
            -
                  thumbnail:  | 
| 99 | 
            +
                  blobId: id,
         | 
| 100 | 
            +
                  thumbnail: element ? element.querySelector('.h-thumbnail') : ''
         | 
| 64 101 | 
             
                }
         | 
| 65 102 | 
             
              }
         | 
| 66 103 |  | 
| @@ -72,7 +109,7 @@ export default class extends Controller { | |
| 72 109 | 
             
              }
         | 
| 73 110 |  | 
| 74 111 | 
             
              selectedItemsCount () {
         | 
| 75 | 
            -
                return this. | 
| 112 | 
            +
                return this.idsValue.length
         | 
| 76 113 | 
             
              }
         | 
| 77 114 |  | 
| 78 115 | 
             
              minSelectedItems () {
         | 
| @@ -105,6 +142,6 @@ export default class extends Controller { | |
| 105 142 | 
             
              }
         | 
| 106 143 |  | 
| 107 144 | 
             
              updateCount () {
         | 
| 108 | 
            -
                this.countTarget.innerHTML = this. | 
| 145 | 
            +
                this.countTarget.innerHTML = this.selectedItemsCount()
         | 
| 109 146 | 
             
              }
         | 
| 110 147 | 
             
            }
         | 
| @@ -10,6 +10,7 @@ import FilterRowController from './controllers/filter_row_controller' | |
| 10 10 | 
             
            import FiltersController from './controllers/filters_controller'
         | 
| 11 11 | 
             
            import FlatpickrController from './controllers/flatpickr_controller'
         | 
| 12 12 | 
             
            import HelloController from './controllers/hello_controller'
         | 
| 13 | 
            +
            import InfiniteScrollerController from './controllers/infinite_scroller_controller'
         | 
| 13 14 | 
             
            import MediaController from './controllers/media_controller'
         | 
| 14 15 | 
             
            import MediaModalController from './controllers/media_modal_controller'
         | 
| 15 16 | 
             
            import NotificationController from './controllers/notification_controller'
         | 
| @@ -35,6 +36,7 @@ export class Headmin { | |
| 35 36 | 
             
                Stimulus.register('filters', FiltersController)
         | 
| 36 37 | 
             
                Stimulus.register('flatpickr', FlatpickrController)
         | 
| 37 38 | 
             
                Stimulus.register('hello', HelloController)
         | 
| 39 | 
            +
                Stimulus.register('infinite-scroller', InfiniteScrollerController)
         | 
| 38 40 | 
             
                Stimulus.register('media', MediaController)
         | 
| 39 41 | 
             
                Stimulus.register('media-modal', MediaModalController)
         | 
| 40 42 | 
             
                Stimulus.register('notification', NotificationController)
         | 
| @@ -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 | 
            -
                   | 
| 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. | 
| 10144 | 
            -
                this. | 
| 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 | 
            -
               | 
| 10188 | 
            -
                 | 
| 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 | 
            -
               | 
| 10234 | 
            -
                this. | 
| 10295 | 
            +
              removeAllDeselectedItems(items) {
         | 
| 10296 | 
            +
                this.removeDeselectedItems(items, this.itemTargets);
         | 
| 10235 10297 | 
             
              }
         | 
| 10236 | 
            -
               | 
| 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. | 
| 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 | 
            -
               | 
| 10360 | 
            +
              handleIdsUpdate(element) {
         | 
| 10361 | 
            +
                if (element.checked) {
         | 
| 10362 | 
            +
                  const 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( | 
| 10392 | 
            +
                    items: this.renderItemsForEvent()
         | 
| 10296 10393 | 
             
                  }
         | 
| 10297 10394 | 
             
                }));
         | 
| 10298 10395 | 
             
              }
         | 
| 10299 10396 | 
             
              triggerFormSubmission() {
         | 
| 10300 10397 | 
             
                this.formTarget.requestSubmit();
         | 
| 10301 10398 | 
             
              }
         | 
| 10302 | 
            -
              renderItemsForEvent( | 
| 10303 | 
            -
                return  | 
| 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 blobId = `#blob_${id}`;
         | 
| 10407 | 
            +
                const element = this.element.querySelector(blobId);
         | 
| 10306 10408 | 
             
                return {
         | 
| 10307 | 
            -
                  blobId:  | 
| 10308 | 
            -
                  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. | 
| 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. | 
| 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,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 | 
            -
             | 
| 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 | 
| 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: "○ #{I18n.t("headmin.filters.operators.is_null")}",
         | 
| 25 25 | 
             
                      is_not_null: "● #{I18n.t("headmin.filters.operators.is_not_null")}",
         | 
| 26 26 | 
             
                      in: "∋ #{I18n.t("headmin.filters.operators.in")}",
         | 
| 27 | 
            -
                      not_in: "∌ #{I18n.t("headmin.filters.operators.not_in")}"
         | 
| 27 | 
            +
                      not_in: "∌ #{I18n.t("headmin.filters.operators.not_in")}",
         | 
| 28 | 
            +
                      between: "↔ #{I18n.t("headmin.filters.operators.between")}",
         | 
| 29 | 
            +
                      not_between: "⥈ #{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 | 
            -
                <%=  | 
| 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 | 
            -
                        <%=  | 
| 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:  | 
| 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,7 @@ | |
| 1 1 | 
             
            <%= turbo_stream.prepend "thumbnails" do %>
         | 
| 2 2 | 
             
              <% @blobs.each do |blob| %>
         | 
| 3 | 
            -
                <%=  | 
| 3 | 
            +
                <%= turbo_frame_tag blob, src: headmin_media_item_thumbnail_path(blob), loading: "lazy" do %>
         | 
| 4 | 
            +
                  <%= render "thumbnail" %>
         | 
| 5 | 
            +
                <% end %>
         | 
| 4 6 | 
             
              <% end %>
         | 
| 5 7 | 
             
            <% 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,7 @@ | |
| 1 | 
            +
            <% auto_load = local_assigns.has_key?(:auto_load) ? local_assigns[:auto_load] : true %>
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            <% unless items.page(params[:page]).last_page? %>
         | 
| 4 | 
            +
              <div id="infinite">
         | 
| 5 | 
            +
                <%= link_to t(".load_more"), path_to_next_page(items), class: "btn btn-primary", data: {turbo_stream: true, controller: auto_load ? "infinite-scroller" : ""} %>
         | 
| 6 | 
            +
              </div>
         | 
| 7 | 
            +
            <% end %>
         | 
    
        data/config/routes.rb
    CHANGED
    
    | @@ -3,8 +3,9 @@ | |
| 3 3 | 
             
            Rails.application.routes.draw do
         | 
| 4 4 | 
             
              namespace(:headmin) do
         | 
| 5 5 | 
             
                get "media", to: "media#index", as: :media
         | 
| 6 | 
            -
                post "media", to: "media#create"
         | 
| 6 | 
            +
                post "media", to: "media#create", as: :new_media
         | 
| 7 7 | 
             
                get "media/:id", to: "media#show", as: :media_item
         | 
| 8 8 | 
             
                patch "media/:id", to: "media#update"
         | 
| 9 | 
            +
                get "media/thumbnail/:id", to: "media#thumbnail", as: :media_item_thumbnail
         | 
| 9 10 | 
             
              end
         | 
| 10 11 | 
             
            end
         | 
    
        data/lib/headmin/version.rb
    CHANGED
    
    
    
        data/package.json
    CHANGED
    
    
    
        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.6. | 
| 4 | 
            +
              version: 0.6.3
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Jef Vlamings
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: exe
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date:  | 
| 11 | 
            +
            date: 2024-09-12 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: closure_tree
         | 
| @@ -105,6 +105,7 @@ files: | |
| 105 105 | 
             
            - app/assets/javascripts/headmin/controllers/filters_controller.js
         | 
| 106 106 | 
             
            - app/assets/javascripts/headmin/controllers/flatpickr_controller.js
         | 
| 107 107 | 
             
            - app/assets/javascripts/headmin/controllers/hello_controller.js
         | 
| 108 | 
            +
            - app/assets/javascripts/headmin/controllers/infinite_scroller_controller.js
         | 
| 108 109 | 
             
            - app/assets/javascripts/headmin/controllers/media_controller.js
         | 
| 109 110 | 
             
            - app/assets/javascripts/headmin/controllers/media_modal_controller.js
         | 
| 110 111 | 
             
            - app/assets/javascripts/headmin/controllers/notification_controller.js
         | 
| @@ -261,6 +262,8 @@ files: | |
| 261 262 | 
             
            - app/models/headmin/.DS_Store
         | 
| 262 263 | 
             
            - app/models/headmin/blocks_view.rb
         | 
| 263 264 | 
             
            - app/models/headmin/filter/association.rb
         | 
| 265 | 
            +
            - app/models/headmin/filter/association_count.rb
         | 
| 266 | 
            +
            - app/models/headmin/filter/association_count_view.rb
         | 
| 264 267 | 
             
            - app/models/headmin/filter/association_view.rb
         | 
| 265 268 | 
             
            - app/models/headmin/filter/base.rb
         | 
| 266 269 | 
             
            - app/models/headmin/filter/base_view.rb
         | 
| @@ -338,6 +341,7 @@ files: | |
| 338 341 | 
             
            - app/views/headmin/dropdown/_list.html.erb
         | 
| 339 342 | 
             
            - app/views/headmin/dropdown/_locale.html.erb
         | 
| 340 343 | 
             
            - app/views/headmin/filters/_association.html.erb
         | 
| 344 | 
            +
            - app/views/headmin/filters/_association_count.html.erb
         | 
| 341 345 | 
             
            - app/views/headmin/filters/_base.html.erb
         | 
| 342 346 | 
             
            - app/views/headmin/filters/_boolean.html.erb
         | 
| 343 347 | 
             
            - app/views/headmin/filters/_date.html.erb
         | 
| @@ -408,17 +412,20 @@ files: | |
| 408 412 | 
             
            - app/views/headmin/layout/_sidebar.html.erb
         | 
| 409 413 | 
             
            - app/views/headmin/layout/sidebar/_bottom.html.erb
         | 
| 410 414 | 
             
            - app/views/headmin/layout/sidebar/_nav.html.erb
         | 
| 411 | 
            -
            - app/views/headmin/media/_item.html.erb
         | 
| 412 415 | 
             
            - app/views/headmin/media/_media_item_modal.html.erb
         | 
| 413 416 | 
             
            - app/views/headmin/media/_modal.html.erb
         | 
| 417 | 
            +
            - app/views/headmin/media/_thumbnail.html.erb
         | 
| 414 418 | 
             
            - app/views/headmin/media/create.turbo_stream.erb
         | 
| 415 419 | 
             
            - app/views/headmin/media/index.html.erb
         | 
| 420 | 
            +
            - app/views/headmin/media/index.turbo_stream.erb
         | 
| 416 421 | 
             
            - app/views/headmin/media/show.html.erb
         | 
| 422 | 
            +
            - app/views/headmin/media/thumbnail.html.erb
         | 
| 417 423 | 
             
            - app/views/headmin/media/update.turbo_stream.erb
         | 
| 418 424 | 
             
            - app/views/headmin/nav/_dropdown.html.erb
         | 
| 419 425 | 
             
            - app/views/headmin/nav/_item.html.erb
         | 
| 420 426 | 
             
            - app/views/headmin/nav/item/_devise.html.erb
         | 
| 421 427 | 
             
            - app/views/headmin/nav/item/_locale.html.erb
         | 
| 428 | 
            +
            - app/views/headmin/pagination/_infinite.html.erb
         | 
| 422 429 | 
             
            - app/views/headmin/pagination/_per_page.html.erb
         | 
| 423 430 | 
             
            - app/views/headmin/pagination/kaminari/_first_page.html.erb
         | 
| 424 431 | 
             
            - app/views/headmin/pagination/kaminari/_gap.html.erb
         | 
| @@ -566,7 +573,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement | |
| 566 573 | 
             
                - !ruby/object:Gem::Version
         | 
| 567 574 | 
             
                  version: '0'
         | 
| 568 575 | 
             
            requirements: []
         | 
| 569 | 
            -
            rubygems_version: 3. | 
| 576 | 
            +
            rubygems_version: 3.5.6
         | 
| 570 577 | 
             
            signing_key:
         | 
| 571 578 | 
             
            specification_version: 4
         | 
| 572 579 | 
             
            summary: Admin component library
         | 
| @@ -1,16 +0,0 @@ | |
| 1 | 
            -
            <div data-media-modal-target="item" title="<%= "#{blob.filename} (#{l(blob.created_at, format: :long)})" %>">
         | 
| 2 | 
            -
              <!-- Input -->
         | 
| 3 | 
            -
              <input
         | 
| 4 | 
            -
                id="media-item-<%= blob.id %>"
         | 
| 5 | 
            -
                type="checkbox"
         | 
| 6 | 
            -
                value="<%= blob.id %>"
         | 
| 7 | 
            -
                <%= "checked" if params[:ids]&.include?(blob.id.to_s) %>
         | 
| 8 | 
            -
                data-action="change->media-modal#inputChange"
         | 
| 9 | 
            -
                data-media-modal-target="idCheckbox"
         | 
| 10 | 
            -
                hidden>
         | 
| 11 | 
            -
             | 
| 12 | 
            -
              <!-- Label -->
         | 
| 13 | 
            -
              <label for="media-item-<%= blob.id %>">
         | 
| 14 | 
            -
                <%= render "headmin/thumbnail", file: blob %>
         | 
| 15 | 
            -
              </label>
         | 
| 16 | 
            -
            </div>
         |