Browse Source

Fixes #3923 - Drag & Drop bulk operations not available in search results

Bola Ahmed Buari 2 years ago
parent
commit
f7d4c04fd4

+ 33 - 9
app/assets/javascripts/app/controllers/search.coffee

@@ -1,5 +1,6 @@
 class App.Search extends App.Controller
   @extend App.PopoverProvidable
+  @extend App.TicketMassUpdatable
 
   elements:
     '.js-search': 'searchInput'
@@ -11,6 +12,8 @@ class App.Search extends App.Controller
     'click .js-tab': 'showTab'
     'input .js-search': 'updateFilledClass'
 
+  @include App.ValidUsersForTicketSelectionMethods
+
   constructor: ->
     super
 
@@ -36,6 +39,11 @@ class App.Search extends App.Controller
       @render()
     )
 
+    load = (data) =>
+      App.Collection.loadAssets(data.assets)
+      @formMeta = data.form_meta
+    @bindId = App.TicketOverviewCollection.bind(load)
+
   meta: =>
     title = @query || App.i18n.translateInline('Extended Search')
 
@@ -54,9 +62,13 @@ class App.Search extends App.Controller
     if @table
       @table.show()
     @navupdate(url: '#search', type: 'menu')
-    return if _.isEmpty(params.query)
 
-    @$('.js-search').val(params.query).trigger('keyup')
+    if !_.isEmpty(params.query)
+      @$('.js-search').val(params.query).trigger('keyup')
+      return
+
+    if @query
+      @search(500, true)
 
   hide: ->
     if @table
@@ -90,11 +102,22 @@ class App.Search extends App.Controller
       @tabs.push tab
 
     # build view
-    @html App.view('search/index')(
+    elLocal = $(App.view('search/index')(
       query: @query
       tabs: @tabs
+    ))
+
+    @controllerTicketBatch.releaseController() if @controllerTicketBatch
+    @controllerTicketBatch = new App.TicketBatch(
+      el:       elLocal.filter('.js-batch-overlay')
+      parent:   @
+      parentEl: elLocal
+      appEl:    @appEl
+      batchSuccess: =>
+        @search(0, true)
     )
 
+    @html elLocal
     if @query
       @search(500, true)
 
@@ -133,6 +156,7 @@ class App.Search extends App.Controller
     @globalSearch.search(
       delay: delay
       query: @query
+      force: force
     )
 
   renderResult: (result = []) =>
@@ -229,16 +253,16 @@ class App.Search extends App.Controller
 
       updateSearch = =>
         callback = =>
-          @search(true)
+          @search(0, true)
         @delay(callback, 100)
 
       @bulkForm.releaseController() if @bulkForm
       @bulkForm = new App.TicketBulkForm(
-        el:        @el.find('.bulkAction')
-        holder:    localeEl
-        view:      @view
-        callback:  updateSearch
-        noSidebar: true
+        el:           @el.find('.bulkAction')
+        holder:       localeEl
+        view:         @view
+        batchSuccess: updateSearch
+        noSidebar:    true
       )
 
       # start bulk action observ

+ 19 - 555
app/assets/javascripts/app/controllers/ticket_overview.coffee

@@ -1,522 +1,22 @@
 class App.TicketOverview extends App.Controller
   @extend App.TicketMassUpdatable
+  @include App.ValidUsersForTicketSelectionMethods
 
   className: 'overviews'
   activeFocus: 'nav'
-  mouse:
-    x: null
-    y: null
-  batchAnimationPaused: false
 
   elements:
-    '.js-batch-overlay':            'batchOverlay'
-    '.js-batch-overlay-backdrop':   'batchOverlayBackdrop'
-    '.js-batch-cancel':             'batchCancel'
-    '.js-batch-macro-circle':       'batchMacroCircle'
-    '.js-batch-assign-circle':      'batchAssignCircle'
-    '.js-batch-assign':             'batchAssign'
-    '.js-batch-assign-inner':       'batchAssignInner'
-    '.js-batch-assign-group':       'batchAssignGroup'
-    '.js-batch-assign-group-name':  'batchAssignGroupName'
-    '.js-batch-assign-group-inner': 'batchAssignGroupInner'
-    '.js-batch-macro':              'batchMacro'
-    '.main':                        'mainContent'
-
-  events:
-    'mousedown .item': 'startDragItem'
-    'mouseenter .js-batch-hover-target': 'highlightBatchEntry'
-    'mouseleave .js-batch-hover-target': 'unhighlightBatchEntry'
-
-  @include App.ValidUsersForTicketSelectionMethods
+    '.main': 'mainContent'
 
   constructor: ->
     super
-    @batchSupport = @permissionCheck('ticket.agent')
     @render()
 
-    # rerender view, e. g. on language change
-    @controllerBind('ui:rerender', =>
-      @renderBatchOverlay()
-    )
     load = (data) =>
       App.Collection.loadAssets(data.assets)
       @formMeta = data.form_meta
     @bindId = App.TicketOverviewCollection.bind(load)
 
-  startDragItem: (event) =>
-    return if !@batchSupport
-    @grabbedItem      = $(event.currentTarget)
-    offset            = @grabbedItem.offset()
-    @batchDragger     = $(App.view('ticket_overview/batch_dragger')())
-    @grabbedItemClone = @grabbedItem.clone()
-    @grabbedItemClone.data('offset', @grabbedItem.offset())
-    @grabbedItemClone.addClass('batch-dragger-item js-main-item')
-    @batchDragger.append @grabbedItemClone
-
-    @batchDragger.data
-      startX: event.pageX
-      startY: event.pageY
-      dx: Math.min(event.pageX - offset.left, 180)
-      dy: event.pageY - offset.top
-      moved: false
-
-    $(document).on 'mousemove.item', @dragItem
-    $(document).one 'mouseup.item', @endDragItem
-    # TODO: fire @cancelDrag on ESC
-
-  dragItem: (event) =>
-    pos = @batchDragger.data()
-    threshold = 3
-    x = event.pageX - pos.dx
-    y = event.pageY - pos.dy
-    dir = if event.pageY > pos.startY then 1 else -1
-
-    if !pos.moved
-      if Math.abs(event.pageY - pos.startY) > threshold || Math.abs(event.pageX - pos.startX) > threshold
-        @batchDragger.data 'moved', true
-        @el.addClass('u-no-userselect')
-        # check grabbed items batch checkbox to make sure its checked
-        # (could be grabbed without checking the checkbox it)
-        @grabbedItemWasntChecked = !@grabbedItem.find('[name="bulk"]').prop('checked')
-        @grabbedItem.find('[name="bulk"]').prop('checked', true)
-        @grabbedItemClone.find('[name="bulk"]').prop('checked', true)
-
-        additionalItems = @el.find('[name="bulk"]:checked').parents('.item').not(@grabbedItem)
-        additionalItemsClones = additionalItems.clone()
-        @draggedItems = @grabbedItemClone.add(additionalItemsClones)
-        # store offsets for later use
-        additionalItemsClones.each (i, item) -> $(@).data('offset', additionalItems.eq(i).offset())
-        @batchDragger.prepend additionalItemsClones.addClass('batch-dragger-item').get().reverse()
-        if(additionalItemsClones.length)
-          @batchDragger.find('.js-batch-dragger-count').text(@draggedItems.length)
-
-        @renderOptions()
-
-        @appEl.append(@batchDragger)
-
-        @draggedItems.each (i, item) ->
-          dx = $(item).data('offset').left - $(item).offset().left - x
-          dy = $(item).data('offset').top - $(item).offset().top - y
-          $.Velocity.hook item, 'translateX', "#{dx}px"
-          $.Velocity.hook item, 'translateY', "#{dy}px"
-
-        @alignDraggedItems(-dir)
-
-        @mouseY = event.pageY
-        @showBatchOverlay()
-      else
-        return
-
-    event.preventDefault()
-
-    $.Velocity.hook @batchDragger, 'translateX', "#{x}px"
-    $.Velocity.hook @batchDragger, 'translateY', "#{y}px"
-
-  endDragItem: (event) =>
-    $(document).off 'mousemove.item'
-    $(document).off 'mouseup.item'
-    pos = @batchDragger.data()
-
-    @clearDelay('clear-hovered-batch-entry')
-
-    if !@hoveredBatchEntry
-      @cleanUpDrag()
-      return
-
-    $.Velocity.hook @batchDragger, 'transformOriginX', "#{pos.dx}px"
-    $.Velocity.hook @batchDragger, 'transformOriginY', "#{pos.dy}px"
-    @hoveredBatchEntry.velocity
-      properties:
-        scale: 1.1
-      options:
-        duration: 200
-        complete: =>
-          if !@hoveredBatchEntry
-            @cleanUpDrag()
-            return
-
-          @hoveredBatchEntry.velocity 'reverse',
-            duration: 200
-            complete: =>
-
-              if !@hoveredBatchEntry
-                @cleanUpDrag()
-                return
-
-              # clean scale
-              action = @hoveredBatchEntry.attr('data-action')
-              id = @hoveredBatchEntry.attr('data-id')
-              groupId = @hoveredBatchEntry.attr('data-group-id')
-              items = @el.find('[name="bulk"]:checked')
-              @hoveredBatchEntry.removeAttr('style')
-              @cleanUpDrag(true)
-
-              @performBatchAction items, action, id, groupId
-    @batchDragger.velocity
-      properties:
-        scale: 0
-      options:
-        duration: 200
-
-  cancelDrag: ->
-    $(document).off 'mousemove.item'
-    $(document).off 'mouseup.item'
-    @cleanUpDrag()
-
-  cleanUpDrag: (success) ->
-    @hideBatchOverlay()
-    @el.removeClass('u-no-userselect')
-    $('.batch-dragger').remove()
-    @hoveredBatchEntry = null
-
-    if @grabbedItemWasntChecked
-      @grabbedItem.find('[name="bulk"]').prop('checked', false)
-
-    if success
-      # uncheck all checked items
-      @el.find('[name="bulk"]:checked').prop('checked', false)
-      @el.find('[name="bulk_all"]').prop('checked', false)
-
-  alignDraggedItems: (dir) ->
-    @draggedItems.velocity
-      properties:
-        translateX: 0
-        translateY: (i) => dir * i * @batchDragger.height()/2
-      options:
-        easing: 'ease-in-out'
-        duration: 300
-
-    @batchDragger.find('.js-batch-dragger-count').velocity
-      properties:
-        translateY: if dir < 0 then 0 else -@batchDragger.height()+8
-      options:
-        easing: 'ease-in-out'
-        duration: 300
-
-  performBatchAction: (items, action, id, groupId) ->
-    ticket_ids = items.toArray().map (item) -> $(item).val()
-
-    switch action
-      when 'macro'
-        path = 'macro'
-        data =
-          ticket_ids: ticket_ids
-          macro_id:   id
-
-      when 'user_assign'
-        path = 'update'
-
-        data =
-          ticket_ids: ticket_ids
-          attributes:
-            owner_id: id
-
-        if !_.isEmpty(groupId)
-          data.attributes.group_id = groupId
-
-      when 'group_assign'
-        path = 'update'
-
-        data =
-          ticket_ids: ticket_ids
-          attributes:
-            group_id: id
-
-    @ajax_mass(path, data)
-
-  showBatchOverlay: ->
-    @batchOverlay.addClass('is-visible')
-    $('html').css('overflow', 'hidden')
-    @batchOverlayBackdrop.velocity { opacity: [1, 0] }, { duration: 500 }
-    @batchMacroOffset = @batchMacro.offset().top + @batchMacro.outerHeight()
-    @batchAssignOffset = @batchAssign.offset().top
-    @batchOverlayShown = true
-    $(document).on 'mousemove.batchoverlay', @controlBatchOverlay
-
-  hideBatchOverlay: ->
-    $(document).off 'mousemove.batchoverlay'
-    @batchOverlayShown = false
-    @batchOverlayBackdrop.velocity { opacity: [0, 1] }, { duration: 300, queue: false }
-    @hideBatchCircles =>
-      @batchOverlay.removeClass('is-visible')
-
-    $('html').css('overflow', '')
-
-    if @batchAssignShown
-      @hideBatchAssign()
-
-    if @batchMacroShown
-      @hideBatchMacro()
-
-    if @batchAssignGroupShown
-      @hideBatchAssignGroup()
-
-  controlBatchOverlay: (event) =>
-    return if @batchAnimationPaused
-    # store to detect if the mouse is hovering a drag-action entry
-    # after an animation ended -> @highlightBatchEntryAtMousePosition
-    @mouse.x = event.pageX
-    @mouse.y = event.pageY
-
-    if @batchAssignGroupShown && @batchAssignGroupOffset != undefined
-      if @mouse.y < @batchAssignGroupOffset
-        @hideBatchAssignGroup()
-        @batchAnimationPaused = true
-      return
-
-    if @mouse.y <= @batchMacroOffset
-      mouseInArea = 'top'
-    else if @mouse.y > @batchMacroOffset && @mouse.y <= @batchAssignOffset
-      mouseInArea = 'middle'
-    else
-      mouseInArea = 'bottom'
-
-    switch mouseInArea
-      when 'top'
-        if !@batchMacroShown
-          @hideBatchCircles()
-          @showBatchMacro()
-          @alignDraggedItems(1)
-
-      when 'middle'
-        if @batchAssignShown
-          @hideBatchAssign()
-
-        if @batchMacroShown
-          @hideBatchMacro()
-
-        if !@batchCirclesShown
-          @showBatchCircles()
-
-      when 'bottom'
-        if !@batchAssignShown
-          @hideBatchCircles()
-          @showBatchAssign()
-          @alignDraggedItems(-1)
-
-  showBatchCircles: ->
-    @batchCirclesShown = true
-
-    @batchMacroCircle.velocity
-      properties:
-        translateY: [0, '-150%']
-        opacity: [1, 0]
-      options:
-        easing: [1,-.55,.2,1.37]
-        duration: 500
-        visibility: 'visible'
-        delay: 200
-
-    @batchAssignCircle.velocity
-      properties:
-        translateY: [0, '150%']
-        opacity: [1, 0]
-      options:
-        easing: [1,-.55,.2,1.37]
-        duration: 500
-        visibility: 'visible'
-        delay: 200
-
-  hideBatchCircles: (callback) ->
-    @batchMacroCircle.velocity
-      properties:
-        translateY: ['-150%', 0]
-        opacity: [0, 1]
-      options:
-        duration: 300
-        visibility: 'hidden'
-        queue: false
-
-    @batchAssignCircle.velocity
-      properties:
-        translateY: ['150%', 0]
-        opacity: [0, 1]
-      options:
-        duration: 300
-        complete: callback
-        visibility: 'hidden'
-        queue: false
-
-    @batchCirclesShown = false
-
-  showBatchAssign: ->
-    return if !@batchOverlayShown # user might have dropped the item already
-    @batchAssignShown = true
-
-    @batchCancel.css
-      top: 0
-      bottom: @batchAssign.height()
-
-    @batchAssign.velocity
-      properties:
-        translateY: [0, '100%']
-        opacity: [1, 0]
-      options:
-        easing: [1,-.55,.2,1.37]
-        duration: 500
-        visibility: 'visible'
-        complete: @highlightBatchEntryAtMousePosition
-
-    @batchCancel.velocity
-      properties:
-        translateY: [0, '100%']
-        opacity: [1, 0]
-      options:
-        easing: [1,-.55,.2,1.37]
-        duration: 500
-        visibility: 'visible'
-
-  hideBatchAssign: ->
-    @batchAssign.velocity
-      properties:
-        translateY: ['100%', 0]
-        opacity: [0, 1]
-      options:
-        duration: 300
-        visibility: 'hidden'
-        queue: false
-        complete: =>
-          $.Velocity.hook @batchAssign, 'translateY', '0%'
-
-    @batchCancel.velocity
-      properties:
-        translateY: ['100%', 0]
-        opacity: [0, 1]
-      options:
-        duration: 300
-        visibility: 'hidden'
-        queue: false
-
-    @batchAssignShown = false
-
-  showBatchAssignGroup: =>
-    return if !@batchOverlayShown # user might have dropped the item already
-    @batchAssignGroupShown = true
-
-    groupId = @hoveredBatchEntry.attr('data-id')
-    group = App.Group.find(groupId)
-
-    @batchAssignGroupName.text group.displayName()
-    @batchAssignGroupInner.html $(App.view('ticket_overview/batch_overlay_user_group')(
-      users: @usersInGroups([groupId])
-      groups: []
-      groupId: groupId
-    ))
-
-    # then adjust the size of the group that it almost overlaps the batch-assign box
-    @batchAssignGroupInner.height(@batchAssignInner.height())
-
-    @batchAssignGroup.velocity
-      properties:
-        translateY: [0, '100%']
-        opacity: [1, 0]
-      options:
-        easing: [1,-.55,.2,1.37]
-        duration: 700
-        visibility: 'visible'
-        complete: =>
-          @highlightBatchEntryAtMousePosition()
-          @batchAssignGroupOffset = @batchAssignGroup.offset().top
-
-  hideBatchAssignGroup: ->
-    @batchAssignGroup.velocity
-      properties:
-        translateY: ['100%', 0]
-        opacity: [0, 1]
-      options:
-        duration: 300
-        visibility: 'hidden'
-        queue: false
-        complete: =>
-          @batchAssignGroupShown = false
-          @batchAssignGroupHovered = false
-          setTimeout (=> @batchAnimationPaused = false), 1000
-
-    @batchAssignGroupOffset = undefined
-
-  showBatchMacro: ->
-    return if !@batchOverlayShown # user might have dropped the item already
-    @batchMacroShown = true
-
-    @batchCancel.css
-      bottom: 0
-      top: @batchMacro.height()
-
-    @batchMacro.velocity
-      properties:
-        translateY: [0, '-100%']
-        opacity: [1, 0]
-      options:
-        easing: [1,-.55,.2,1.37]
-        duration: 500
-        visibility: 'visible'
-        complete: @highlightBatchEntryAtMousePosition
-
-    @batchCancel.velocity
-      properties:
-        translateY: [0, '-100%']
-        opacity: [1, 0]
-      options:
-        easing: [1,-.55,.2,1.37]
-        duration: 500
-        visibility: 'visible'
-
-  hideBatchMacro: ->
-    @batchMacro.velocity
-      properties:
-        translateY: ['-100%', 0]
-        opacity: [0, 1]
-      options:
-        duration: 300
-        visibility: 'hidden'
-        queue: false
-        complete: =>
-          $.Velocity.hook @batchMacro, 'translateY', '0%'
-
-    @batchCancel.velocity
-      properties:
-        translateY: ['-100%', 0]
-        opacity: [0, 1]
-      options:
-        duration: 300
-        visibility: 'hidden'
-        queue: false
-
-    @batchMacroShown = false
-
-  highlightBatchEntryAtMousePosition: =>
-    entryAtPoint = $(document.elementFromPoint(@mouse.x, @mouse.y)).closest('.js-batch-overlay-entry .avatar')
-    if(entryAtPoint.length)
-      @hoveredBatchEntry = entryAtPoint.closest('.js-batch-overlay-entry').addClass('is-hovered')
-
-  highlightBatchEntry: (event) ->
-    @clearDelay('clear-hovered-batch-entry')
-    @hoveredBatchEntry = $(event.currentTarget).closest('.js-batch-overlay-entry').addClass('is-hovered')
-
-    if @hoveredBatchEntry.attr('data-action') is 'group_assign'
-      @batchAssignGroupHintTimeout = setTimeout @blinkBatchEntry, 800
-      @batchAssignGroupTimeout = setTimeout @showBatchAssignGroup, 900
-
-  unhighlightBatchEntry: (event) ->
-    return if !@hoveredBatchEntry
-    if @hoveredBatchEntry.attr('data-action') is 'group_assign'
-      if @batchAssignGroupTimeout
-        clearTimeout @batchAssignGroupTimeout
-      if @batchAssignGroupHintTimeout
-        clearTimeout @batchAssignGroupHintTimeout
-
-    @hoveredBatchEntry.removeClass('is-hovered')
-    delay = =>
-      @hoveredBatchEntry = null
-    @delay(delay, 800, 'clear-hovered-batch-entry')
-
-  blinkBatchEntry: =>
-    @hoveredBatchEntry
-      .velocity({ opacity: [0.5, 1] }, { duration: 120 })
-      .velocity({ opacity: [1, 0.5] }, { duration: 60, delay: 40 })
-      .velocity({ opacity: [0.5, 1] }, { duration: 120 })
-      .velocity({ opacity: [1, 0.5] }, { duration: 60, delay: 40 })
-
   render: ->
     elLocal = $(App.view('ticket_overview/index')())
 
@@ -533,6 +33,16 @@ class App.TicketOverview extends App.Controller
       view: @view
     )
 
+    @controllerTicketBatch.releaseController() if @controllerTicketBatch
+    @controllerTicketBatch = new App.TicketBatch(
+      el:           elLocal.filter('.js-batch-overlay')
+      parent:       @
+      parentEl:     elLocal
+      appEl:        @appEl
+      batchSuccess: =>
+        @render()
+    )
+
     @contentController.releaseController() if @contentController
     @contentController = new Table(
       el:          elLocal.find('.overview-table')
@@ -541,8 +51,6 @@ class App.TicketOverview extends App.Controller
       keyboardOff: @keyboardOff
     )
 
-    @renderBatchOverlay(elLocal.filter('.js-batch-overlay'))
-
     @html elLocal
 
     @$('.main').on('click', =>
@@ -559,54 +67,6 @@ class App.TicketOverview extends App.Controller
       @delay(update, 2800, 'overview:fetch')
     )
 
-  renderBatchOverlay: (elLocal) =>
-    if elLocal
-      elLocal.html( App.view('ticket_overview/batch_overlay')() )
-      return
-    @batchOverlay.html( App.view('ticket_overview/batch_overlay')() )
-    @refreshElements()
-
-  renderOptions: =>
-    @renderOptionsGroups()
-    @renderOptionsMacros()
-
-  renderOptionsGroups: =>
-    @batchAssignInner.html $(App.view('ticket_overview/batch_overlay_user_group')(
-      @validUsersForTicketSelection()
-    ))
-
-  renderOptionsMacros: =>
-
-    @possibleMacros = []
-    macros          = App.Macro.getList()
-
-    items = @el.find('[name="bulk"]:checked')
-
-    group_ids =[]
-    for item in items
-      ticket = App.Ticket.find($(item).val())
-      group_ids.push ticket.group_id
-
-    group_ids = _.uniq(group_ids)
-
-    for macro in macros
-
-      # push if no group_ids exists
-      if _.isEmpty(macro.group_ids) && !_.includes(@possibleMacros, macro)
-        @possibleMacros.push macro
-
-      # push if group_ids are equal
-      if _.isEqual(macro.group_ids, group_ids) && !_.includes(@possibleMacros, macro)
-        @possibleMacros.push macro
-
-      # push if all group_ids of tickets are in macro.group_ids
-      if !_.isEmpty(macro.group_ids) && _.isEmpty(_.difference(group_ids,macro.group_ids)) && !_.includes(@possibleMacros, macro)
-        @possibleMacros.push macro
-
-    @batchMacro.html $(App.view('ticket_overview/batch_overlay_macro')(
-      macros: @possibleMacros
-    ))
-
   active: (state) =>
     return @shown if state is undefined
     @shown = state
@@ -1058,6 +518,8 @@ class Table extends App.Controller
 
         # open ticket via task manager to provide task with overview info
         ticket = App.Ticket.findNative(id)
+        return if !ticket
+
         App.TaskManager.execute(
           key:        "Ticket-#{ticket.id}"
           controller: 'TicketZoom'
@@ -1228,9 +690,11 @@ class Table extends App.Controller
 
     @bulkForm.releaseController() if @bulkForm
     @bulkForm = new App.TicketBulkForm(
-      el:     @el.find('.bulkAction')
-      holder: @el
-      view:   @view
+      el:           @el.find('.bulkAction')
+      holder:       @el
+      view:         @view
+      batchSuccess: =>
+        @render()
     )
 
     # start bulk action observ

+ 548 - 0
app/assets/javascripts/app/controllers/widget/ticket_batch.coffee

@@ -0,0 +1,548 @@
+class App.TicketBatch extends App.Controller
+  requiredPermission: 'ticket.agent'
+
+  mouse:
+    x: null
+    y: null
+  batchAnimationPaused: false
+
+  elements:
+    '.js-batch-overlay-backdrop':   'batchOverlayBackdrop'
+    '.js-batch-cancel':             'batchCancel'
+    '.js-batch-macro-circle':       'batchMacroCircle'
+    '.js-batch-assign-circle':      'batchAssignCircle'
+    '.js-batch-assign':             'batchAssign'
+    '.js-batch-assign-inner':       'batchAssignInner'
+    '.js-batch-assign-group':       'batchAssignGroup'
+    '.js-batch-assign-group-name':  'batchAssignGroupName'
+    '.js-batch-assign-group-inner': 'batchAssignGroupInner'
+    '.js-batch-macro':              'batchMacro'
+
+  events:
+    'mouseenter .js-batch-hover-target': 'highlightBatchEntry'
+    'mouseleave .js-batch-hover-target': 'unhighlightBatchEntry'
+
+  constructor: ->
+    super
+
+    # rerender view, e. g. on language change
+    @controllerBind('ui:rerender', @render)
+    @render()
+
+  render: =>
+    @html App.view('ticket_overview/batch_overlay')()
+    @parentEl.off('mousedown.TicketBatch').on('mousedown.TicketBatch', '.item', @startDragItem)
+
+  renderOptions: =>
+    @renderOptionsGroups()
+    @renderOptionsMacros()
+
+  renderOptionsGroups: =>
+    @batchAssignInner.html $(App.view('ticket_overview/batch_overlay_user_group')(
+      @parent.validUsersForTicketSelection()
+    ))
+
+  renderOptionsMacros: =>
+
+    @possibleMacros = []
+    macros          = App.Macro.getList()
+
+    items = @parentEl.find('[name="bulk"]:checked')
+
+    group_ids =[]
+    for item in items
+      ticket = App.Ticket.find($(item).val())
+      group_ids.push ticket.group_id
+
+    group_ids = _.uniq(group_ids)
+
+    for macro in macros
+
+      # push if no group_ids exists
+      if _.isEmpty(macro.group_ids) && !_.includes(@possibleMacros, macro)
+        @possibleMacros.push macro
+
+      # push if group_ids are equal
+      if _.isEqual(macro.group_ids, group_ids) && !_.includes(@possibleMacros, macro)
+        @possibleMacros.push macro
+
+      # push if all group_ids of tickets are in macro.group_ids
+      if !_.isEmpty(macro.group_ids) && _.isEmpty(_.difference(group_ids,macro.group_ids)) && !_.includes(@possibleMacros, macro)
+        @possibleMacros.push macro
+
+    @batchMacro.html $(App.view('ticket_overview/batch_overlay_macro')(
+      macros: @possibleMacros
+    ))
+
+  startDragItem: (event) =>
+    @grabbedItem      = $(event.currentTarget)
+    offset            = @grabbedItem.offset()
+    @batchDragger     = $(App.view('ticket_overview/batch_dragger')())
+    @grabbedItemClone = @grabbedItem.clone()
+    @grabbedItemClone.data('offset', @grabbedItem.offset())
+    @grabbedItemClone.addClass('batch-dragger-item js-main-item')
+    @batchDragger.append @grabbedItemClone
+
+    @batchDragger.data
+      startX: event.pageX
+      startY: event.pageY
+      dx: Math.min(event.pageX - offset.left, 180)
+      dy: event.pageY - offset.top
+      moved: false
+
+    $(document).on 'mousemove.item', @dragItem
+    $(document).one 'mouseup.item', @endDragItem
+    # TODO: fire @cancelDrag on ESC
+
+  dragItem: (event) =>
+    pos = @batchDragger.data()
+    threshold = 3
+    x = event.pageX - pos.dx
+    y = event.pageY - pos.dy
+    dir = if event.pageY > pos.startY then 1 else -1
+
+    if !pos.moved
+      if Math.abs(event.pageY - pos.startY) > threshold || Math.abs(event.pageX - pos.startX) > threshold
+        @batchDragger.data 'moved', true
+        @el.addClass('u-no-userselect')
+        # check grabbed items batch checkbox to make sure its checked
+        # (could be grabbed without checking the checkbox it)
+        @grabbedItemWasntChecked = !@grabbedItem.find('[name="bulk"]').prop('checked')
+        @grabbedItem.find('[name="bulk"]').prop('checked', true)
+        @grabbedItemClone.find('[name="bulk"]').prop('checked', true)
+
+        additionalItems = @el.find('[name="bulk"]:checked').parents('.item').not(@grabbedItem)
+        additionalItemsClones = additionalItems.clone()
+        @draggedItems = @grabbedItemClone.add(additionalItemsClones)
+        # store offsets for later use
+        additionalItemsClones.each (i, item) -> $(@).data('offset', additionalItems.eq(i).offset())
+        @batchDragger.prepend additionalItemsClones.addClass('batch-dragger-item').get().reverse()
+        if(additionalItemsClones.length)
+          @batchDragger.find('.js-batch-dragger-count').text(@draggedItems.length)
+
+        @renderOptions()
+
+        @appEl.append(@batchDragger)
+
+        @draggedItems.each (i, item) ->
+          dx = $(item).data('offset').left - $(item).offset().left - x
+          dy = $(item).data('offset').top - $(item).offset().top - y
+          $.Velocity.hook item, 'translateX', "#{dx}px"
+          $.Velocity.hook item, 'translateY', "#{dy}px"
+
+        @alignDraggedItems(-dir)
+
+        @mouseY = event.pageY
+        @showBatchOverlay()
+      else
+        return
+
+    event.preventDefault()
+
+    $.Velocity.hook @batchDragger, 'translateX', "#{x}px"
+    $.Velocity.hook @batchDragger, 'translateY', "#{y}px"
+
+  endDragItem: (event) =>
+    $(document).off 'mousemove.item'
+    $(document).off 'mouseup.item'
+    pos = @batchDragger.data()
+
+    @clearDelay('clear-hovered-batch-entry')
+
+    if !@hoveredBatchEntry
+      @cleanUpDrag()
+      return
+
+    $.Velocity.hook @batchDragger, 'transformOriginX', "#{pos.dx}px"
+    $.Velocity.hook @batchDragger, 'transformOriginY', "#{pos.dy}px"
+    @hoveredBatchEntry.velocity
+      properties:
+        scale: 1.1
+      options:
+        duration: 200
+        complete: =>
+          if !@hoveredBatchEntry
+            @cleanUpDrag()
+            return
+
+          @hoveredBatchEntry.velocity 'reverse',
+            duration: 200
+            complete: =>
+
+              if !@hoveredBatchEntry
+                @cleanUpDrag()
+                return
+
+              # clean scale
+              action = @hoveredBatchEntry.attr('data-action')
+              id = @hoveredBatchEntry.attr('data-id')
+              groupId = @hoveredBatchEntry.attr('data-group-id')
+              items = @parentEl.find('[name="bulk"]:checked')
+              @hoveredBatchEntry.removeAttr('style')
+              @cleanUpDrag(true)
+
+              @performBatchAction items, action, id, groupId
+    @batchDragger.velocity
+      properties:
+        scale: 0
+      options:
+        duration: 200
+
+  cancelDrag: ->
+    $(document).off 'mousemove.item'
+    $(document).off 'mouseup.item'
+    @cleanUpDrag()
+
+  cleanUpDrag: (success) ->
+    @hideBatchOverlay()
+    @el.removeClass('u-no-userselect')
+    $('.batch-dragger').remove()
+    @hoveredBatchEntry = null
+
+    if @grabbedItemWasntChecked
+      @grabbedItem.find('[name="bulk"]').prop('checked', false)
+
+    if success
+      # uncheck all checked items
+      @el.find('[name="bulk"]:checked').prop('checked', false)
+      @el.find('[name="bulk_all"]').prop('checked', false)
+
+  alignDraggedItems: (dir) ->
+    @draggedItems.velocity
+      properties:
+        translateX: 0
+        translateY: (i) => dir * i * @batchDragger.height()/2
+      options:
+        easing: 'ease-in-out'
+        duration: 300
+
+    @batchDragger.find('.js-batch-dragger-count').velocity
+      properties:
+        translateY: if dir < 0 then 0 else -@batchDragger.height()+8
+      options:
+        easing: 'ease-in-out'
+        duration: 300
+
+  performBatchAction: (items, action, id, groupId) ->
+    ticket_ids = items.toArray().map (item) -> $(item).val()
+
+    switch action
+      when 'macro'
+        path = 'macro'
+        data =
+          ticket_ids: ticket_ids
+          macro_id:   id
+
+      when 'user_assign'
+        path = 'update'
+
+        data =
+          ticket_ids: ticket_ids
+          attributes:
+            owner_id: id
+
+        if !_.isEmpty(groupId)
+          data.attributes.group_id = groupId
+
+      when 'group_assign'
+        path = 'update'
+
+        data =
+          ticket_ids: ticket_ids
+          attributes:
+            group_id: id
+
+    @parent.ajax_mass(path, data, @batchSuccess)
+
+  showBatchOverlay: ->
+    @el.addClass('is-visible')
+    $('html').css('overflow', 'hidden')
+    @batchOverlayBackdrop.velocity { opacity: [1, 0] }, { duration: 500 }
+    @batchMacroOffset = @batchMacro.offset().top + @batchMacro.outerHeight()
+    @batchAssignOffset = @batchAssign.offset().top
+    @batchOverlayShown = true
+    $(document).on 'mousemove.batchoverlay', @controlBatchOverlay
+
+  hideBatchOverlay: ->
+    $(document).off 'mousemove.batchoverlay'
+    @batchOverlayShown = false
+    @batchOverlayBackdrop.velocity { opacity: [0, 1] }, { duration: 300, queue: false }
+    @hideBatchCircles =>
+      @el.removeClass('is-visible')
+
+    $('html').css('overflow', '')
+
+    if @batchAssignShown
+      @hideBatchAssign()
+
+    if @batchMacroShown
+      @hideBatchMacro()
+
+    if @batchAssignGroupShown
+      @hideBatchAssignGroup()
+
+  controlBatchOverlay: (event) =>
+    return if @batchAnimationPaused
+    # store to detect if the mouse is hovering a drag-action entry
+    # after an animation ended -> @highlightBatchEntryAtMousePosition
+    @mouse.x = event.pageX
+    @mouse.y = event.pageY
+
+    if @batchAssignGroupShown && @batchAssignGroupOffset != undefined
+      if @mouse.y < @batchAssignGroupOffset
+        @hideBatchAssignGroup()
+        @batchAnimationPaused = true
+      return
+
+    if @mouse.y <= @batchMacroOffset
+      mouseInArea = 'top'
+    else if @mouse.y > @batchMacroOffset && @mouse.y <= @batchAssignOffset
+      mouseInArea = 'middle'
+    else
+      mouseInArea = 'bottom'
+
+    switch mouseInArea
+      when 'top'
+        if !@batchMacroShown
+          @hideBatchCircles()
+          @showBatchMacro()
+          @alignDraggedItems(1)
+
+      when 'middle'
+        if @batchAssignShown
+          @hideBatchAssign()
+
+        if @batchMacroShown
+          @hideBatchMacro()
+
+        if !@batchCirclesShown
+          @showBatchCircles()
+
+      when 'bottom'
+        if !@batchAssignShown
+          @hideBatchCircles()
+          @showBatchAssign()
+          @alignDraggedItems(-1)
+
+  showBatchCircles: ->
+    @batchCirclesShown = true
+
+    @batchMacroCircle.velocity
+      properties:
+        translateY: [0, '-150%']
+        opacity: [1, 0]
+      options:
+        easing: [1,-.55,.2,1.37]
+        duration: 500
+        visibility: 'visible'
+        delay: 200
+
+    @batchAssignCircle.velocity
+      properties:
+        translateY: [0, '150%']
+        opacity: [1, 0]
+      options:
+        easing: [1,-.55,.2,1.37]
+        duration: 500
+        visibility: 'visible'
+        delay: 200
+
+  hideBatchCircles: (callback) ->
+    @batchMacroCircle.velocity
+      properties:
+        translateY: ['-150%', 0]
+        opacity: [0, 1]
+      options:
+        duration: 300
+        visibility: 'hidden'
+        queue: false
+
+    @batchAssignCircle.velocity
+      properties:
+        translateY: ['150%', 0]
+        opacity: [0, 1]
+      options:
+        duration: 300
+        complete: callback
+        visibility: 'hidden'
+        queue: false
+
+    @batchCirclesShown = false
+
+  showBatchAssign: ->
+    return if !@batchOverlayShown # user might have dropped the item already
+    @batchAssignShown = true
+
+    @batchCancel.css
+      top: 0
+      bottom: @batchAssign.height()
+
+    @batchAssign.velocity
+      properties:
+        translateY: [0, '100%']
+        opacity: [1, 0]
+      options:
+        easing: [1,-.55,.2,1.37]
+        duration: 500
+        visibility: 'visible'
+        complete: @highlightBatchEntryAtMousePosition
+
+    @batchCancel.velocity
+      properties:
+        translateY: [0, '100%']
+        opacity: [1, 0]
+      options:
+        easing: [1,-.55,.2,1.37]
+        duration: 500
+        visibility: 'visible'
+
+  hideBatchAssign: ->
+    @batchAssign.velocity
+      properties:
+        translateY: ['100%', 0]
+        opacity: [0, 1]
+      options:
+        duration: 300
+        visibility: 'hidden'
+        queue: false
+        complete: =>
+          $.Velocity.hook @batchAssign, 'translateY', '0%'
+
+    @batchCancel.velocity
+      properties:
+        translateY: ['100%', 0]
+        opacity: [0, 1]
+      options:
+        duration: 300
+        visibility: 'hidden'
+        queue: false
+
+    @batchAssignShown = false
+
+  showBatchAssignGroup: =>
+    return if !@batchOverlayShown # user might have dropped the item already
+    @batchAssignGroupShown = true
+
+    groupId = @hoveredBatchEntry.attr('data-id')
+    group = App.Group.find(groupId)
+
+    @batchAssignGroupName.text group.displayName()
+    @batchAssignGroupInner.html $(App.view('ticket_overview/batch_overlay_user_group')(
+      users: @parent.usersInGroups([groupId])
+      groups: []
+      groupId: groupId
+    ))
+
+    # then adjust the size of the group that it almost overlaps the batch-assign box
+    @batchAssignGroupInner.height(@batchAssignInner.height())
+
+    @batchAssignGroup.velocity
+      properties:
+        translateY: [0, '100%']
+        opacity: [1, 0]
+      options:
+        easing: [1,-.55,.2,1.37]
+        duration: 700
+        visibility: 'visible'
+        complete: =>
+          @highlightBatchEntryAtMousePosition()
+          @batchAssignGroupOffset = @batchAssignGroup.offset().top
+
+  hideBatchAssignGroup: ->
+    @batchAssignGroup.velocity
+      properties:
+        translateY: ['100%', 0]
+        opacity: [0, 1]
+      options:
+        duration: 300
+        visibility: 'hidden'
+        queue: false
+        complete: =>
+          @batchAssignGroupShown = false
+          @batchAssignGroupHovered = false
+          setTimeout (=> @batchAnimationPaused = false), 1000
+
+    @batchAssignGroupOffset = undefined
+
+  showBatchMacro: ->
+    return if !@batchOverlayShown # user might have dropped the item already
+    @batchMacroShown = true
+
+    @batchCancel.css
+      bottom: 0
+      top: @batchMacro.height()
+
+    @batchMacro.velocity
+      properties:
+        translateY: [0, '-100%']
+        opacity: [1, 0]
+      options:
+        easing: [1,-.55,.2,1.37]
+        duration: 500
+        visibility: 'visible'
+        complete: @highlightBatchEntryAtMousePosition
+
+    @batchCancel.velocity
+      properties:
+        translateY: [0, '-100%']
+        opacity: [1, 0]
+      options:
+        easing: [1,-.55,.2,1.37]
+        duration: 500
+        visibility: 'visible'
+
+  hideBatchMacro: ->
+    @batchMacro.velocity
+      properties:
+        translateY: ['-100%', 0]
+        opacity: [0, 1]
+      options:
+        duration: 300
+        visibility: 'hidden'
+        queue: false
+        complete: =>
+          $.Velocity.hook @batchMacro, 'translateY', '0%'
+
+    @batchCancel.velocity
+      properties:
+        translateY: ['-100%', 0]
+        opacity: [0, 1]
+      options:
+        duration: 300
+        visibility: 'hidden'
+        queue: false
+
+    @batchMacroShown = false
+
+  highlightBatchEntryAtMousePosition: =>
+    entryAtPoint = $(document.elementFromPoint(@mouse.x, @mouse.y)).closest('.js-batch-overlay-entry .avatar')
+    if(entryAtPoint.length)
+      @hoveredBatchEntry = entryAtPoint.closest('.js-batch-overlay-entry').addClass('is-hovered')
+
+  highlightBatchEntry: (event) =>
+    @clearDelay('clear-hovered-batch-entry')
+    @hoveredBatchEntry = $(event.currentTarget).closest('.js-batch-overlay-entry').addClass('is-hovered')
+
+    if @hoveredBatchEntry.attr('data-action') is 'group_assign'
+      @batchAssignGroupHintTimeout = setTimeout @blinkBatchEntry, 800
+      @batchAssignGroupTimeout = setTimeout @showBatchAssignGroup, 900
+
+  unhighlightBatchEntry: (event) ->
+    return if !@hoveredBatchEntry
+    if @hoveredBatchEntry.attr('data-action') is 'group_assign'
+      if @batchAssignGroupTimeout
+        clearTimeout @batchAssignGroupTimeout
+      if @batchAssignGroupHintTimeout
+        clearTimeout @batchAssignGroupHintTimeout
+
+    @hoveredBatchEntry.removeClass('is-hovered')
+    delay = =>
+      @hoveredBatchEntry = null
+    @delay(delay, 800, 'clear-hovered-batch-entry')
+
+  blinkBatchEntry: =>
+    @hoveredBatchEntry
+      .velocity({ opacity: [0.5, 1] }, { duration: 120 })
+      .velocity({ opacity: [1, 0.5] }, { duration: 60, delay: 40 })
+      .velocity({ opacity: [0.5, 1] }, { duration: 120 })
+      .velocity({ opacity: [1, 0.5] }, { duration: 60, delay: 40 })

+ 1 - 1
app/assets/javascripts/app/controllers/widget/ticket_bulk_form.coffee

@@ -240,7 +240,7 @@ class App.TicketBulkForm extends App.Controller
 
     @ajax_mass_update(data, =>
       @holder.find('.table').find('[name="bulk"]:checked').prop('checked', false)
-      @render()
+      @batchSuccess()
       @hide()
     )
 

+ 1 - 1
app/assets/javascripts/app/lib/app_post/global_search.coffee

@@ -85,7 +85,7 @@ class App.GlobalSearch extends App.Controller
           params.callbackMatch()
 
       # if result hasn't changed, do not rerender
-      if @lastQuery is query && @searchResultCache[query]
+      if !params.force && @lastQuery is query && @searchResultCache[query]
         diff = difference(@searchResultCache[query].result, result)
         if _.isEmpty(diff)
           return

+ 1 - 2
app/assets/javascripts/app/lib/mixins/valid_users_for_ticket_selection_methods.coffee

@@ -1,6 +1,6 @@
 App.ValidUsersForTicketSelectionMethods =
   validUsersForTicketSelection: ->
-    items = $('.content.active .table-overview .table').find('[name="bulk"]:checked')
+    items = $('.content.active .main .table').find('[name="bulk"]:checked')
 
     # we want to display all users for which we can assign the tickets directly
     # for this we need to get the groups of all selected tickets
@@ -17,7 +17,6 @@ App.ValidUsersForTicketSelectionMethods =
     group_ids     = _.keys(@formMeta?.dependencies?.group_id)
     groups        = App.Group.findAll(group_ids)
     groups_sorted = _.sortBy(groups, (group) -> group.name)
-
     # get the number of visible users per group
     # from the TicketOverviewCollection
     # (filled for e.g. the TicketCreation or TicketZoom assignment)

+ 1 - 0
app/assets/javascripts/app/views/search/index.jst.eco

@@ -26,3 +26,4 @@
 </div>
 
 <div class="bulkAction hide"></div>
+<div class="batch-overlay js-batch-overlay"></div>

+ 181 - 26
spec/system/search_spec.rb

@@ -3,10 +3,15 @@
 require 'rails_helper'
 
 RSpec.describe 'Search', type: :system, authenticated: true, searchindex: true do
-  let(:users_group) { Group.find_by(name: 'Users') }
-  let(:ticket_1)    { create(:ticket, title: 'Testing Ticket 1', group: users_group) }
-  let(:ticket_2)    { create(:ticket, title: 'Testing Ticket 2', group: users_group) }
-  let(:note)        { 'Test note' }
+  let(:group_1)              { create :group }
+  let(:group_2)              { create :group }
+  let(:macro_without_group)  { create :macro }
+  let(:macro_note)           { create :macro, name: 'Macro note', perform: { 'article.note'=>{ 'body' => 'macro body', 'internal' => 'true', 'subject' => 'macro note' } } }
+  let(:macro_group1)         { create :macro, groups: [group_1] }
+  let(:macro_group2)         { create :macro, groups: [group_2] }
+  let(:ticket_1)             { create :ticket, title: 'Testing Ticket 1', group: group_1 }
+  let(:ticket_2)             { create :ticket, title: 'Testing Ticket 2', group: group_2 }
+  let(:note)                 { 'Test note' }
 
   before do
     ticket_1 && ticket_2
@@ -23,7 +28,14 @@ RSpec.describe 'Search', type: :system, authenticated: true, searchindex: true d
     end
   end
 
-  context 'with ticket search result' do
+  context 'with ticket search result', authenticated_as: :authenticate do
+    let(:agent) { create(:agent, groups: Group.all) }
+
+    def authenticate
+      ticket_1 && ticket_2
+      agent
+    end
+
     before do
       fill_in id: 'global-search', with: 'Testing'
       click_on 'Show Search Details'
@@ -117,11 +129,148 @@ RSpec.describe 'Search', type: :system, authenticated: true, searchindex: true d
         end.not_to raise_error
       end
     end
+
+    context 'with drag and drop' do
+      context 'when checked tickets are dragged' do
+        it 'shows the batch actions' do
+          within(:active_content, '.main .table') do
+            # get element to move
+            element = page.find(:table_row, ticket_1.id).native
+            click_and_hold(element)
+            # move element a bit to display batch actions
+            move_mouse_by(0, 5)
+            # move mouse again to trigger the event for chrome
+            move_mouse_by(0, 7)
+          end
+
+          expect(page).to have_selector('.batch-overlay-circle--top.js-batch-macro-circle')
+            .and(have_selector('.batch-overlay-circle--bottom.js-batch-assign-circle'))
+        end
+      end
+    end
+  end
+
+  context 'with ticket search result for macros bulk action', authenticated_as: :authenticate do
+    let(:group_3)      { create :group }
+    let(:search_query) { 'Testing' }
+    let(:ticket_3)     { create :ticket, title: 'Testing Ticket 3', group: group_3 }
+    let(:agent)        { create(:agent, groups: Group.all) }
+
+    before do
+      fill_in id: 'global-search', with: search_query
+      click_on 'Show Search Details'
+
+      find('[data-tab-content=Ticket]').click
+    end
+
+    describe 'group-dependent macros' do
+      def authenticate
+        ticket_1 && ticket_2 && ticket_3
+        macro_without_group && macro_group1 && macro_group2
+        agent
+      end
+
+      it 'shows only non-group macro when ticket does not match any group macros' do
+        within(:active_content) do
+          display_macro_batches ticket_3
+
+          expect(page).to have_selector(:macro_batch, macro_without_group.id)
+            .and(have_no_selector(:macro_batch, macro_group1.id))
+            .and(have_no_selector(:macro_batch, macro_group2.id))
+        end
+      end
+
+      it 'shows non-group and matching group macros for matching ticket' do
+        within(:active_content) do
+          display_macro_batches ticket_1
+
+          expect(page).to have_selector(:macro_batch, macro_without_group.id)
+            .and(have_selector(:macro_batch, macro_group1.id))
+            .and(have_no_selector(:macro_batch, macro_group2.id))
+        end
+      end
+    end
+
+    context 'with macro batch overlay' do
+      shared_examples "adding 'small' class to macro element" do
+        it 'adds a "small" class to the macro element' do
+          within(:active_content) do
+            display_macro_batches ticket_1
+
+            expect(page).to have_selector('.batch-overlay-macro-entry.small')
+          end
+        end
+      end
+
+      shared_examples "not adding 'small' class to macro element" do
+        it 'does not add a "small" class to the macro element' do
+          within(:active_content) do
+            display_macro_batches ticket_1
+
+            expect(page).to have_no_selector('.batch-overlay-macro-entry.small')
+          end
+        end
+      end
+
+      shared_examples 'showing all macros' do
+        it 'shows all macros' do
+          within(:active_content) do
+            display_macro_batches ticket_1
+
+            expect(page).to have_selector('.batch-overlay-macro-entry', count: all)
+          end
+        end
+      end
+
+      shared_examples 'showing some macros' do |count|
+        it 'shows all macros' do
+          within(:active_content) do
+            display_macro_batches ticket_1
+
+            expect(page).to have_selector('.batch-overlay-macro-entry', count: count)
+          end
+        end
+      end
+
+      def authenticate
+        ticket_1 && ticket_2
+        Macro.destroy_all && (create_list :macro, all)
+        agent
+      end
+
+      context 'with few macros' do
+        let(:all) { 15 }
+
+        context 'when on large screen', screen_size: :desktop do
+          it_behaves_like 'showing all macros'
+          it_behaves_like "not adding 'small' class to macro element"
+        end
+
+        context 'when on small screen', screen_size: :tablet do
+          it_behaves_like 'showing all macros'
+          it_behaves_like "not adding 'small' class to macro element"
+        end
+
+      end
+
+      context 'with many macros' do
+        let(:all) { 50 }
+
+        context 'when on large screen', screen_size: :desktop do
+          it_behaves_like 'showing some macros', 32
+        end
+
+        context 'when on small screen', screen_size: :tablet do
+          it_behaves_like 'showing some macros', 24
+          it_behaves_like "adding 'small' class to macro element"
+        end
+      end
+    end
   end
 
   context 'Organization members', authenticated_as: :authenticate do
     let(:organization) { create(:organization) }
-    let(:members) { organization.members.order(id: :asc) }
+    let(:members)      { organization.members.order(id: :asc) }
 
     def authenticate
       create_list(:customer, 50, organization: organization)
@@ -173,7 +322,13 @@ RSpec.describe 'Search', type: :system, authenticated: true, searchindex: true d
     end
   end
 
-  describe 'Search is not triggered/updated if url of search is updated new search item or new search is triggered via global search #3873' do
+  describe 'Search is not triggered/updated if url of search is updated new search item or new search is triggered via global search #3873', authenticated_as: :authenticate do
+    let(:agent) { create(:agent, groups: Group.all) }
+
+    def authenticate
+      ticket_1 && ticket_2
+      agent
+    end
 
     context 'when search changed via input box' do
       before do
@@ -235,7 +390,7 @@ RSpec.describe 'Search', type: :system, authenticated: true, searchindex: true d
 
   context 'Assign user to multiple organizations #1573', authenticated_as: :authenticate do
     let(:organizations) { create_list(:organization, 20) }
-    let(:customer) { create(:customer, organization: organizations[0], organizations: organizations[1..]) }
+    let(:customer)      { create(:customer, organization: organizations[0], organizations: organizations[1..]) }
 
     context 'when agent' do
       def authenticate
@@ -271,24 +426,24 @@ RSpec.describe 'Search', type: :system, authenticated: true, searchindex: true d
   end
 
   describe 'Searches display all groups and owners on bulk selections #4054', authenticated_as: :authenticate do
-    let(:group1)    { create(:group) }
-    let(:group2)    { create(:group) }
-    let(:agent1)    { create(:agent, groups: [group1]) }
-    let(:agent2)    { create(:agent, groups: [group2]) }
-    let(:agent_all) { create(:agent, groups: [group1, group2]) }
-    let(:ticket1)   { create(:ticket, group: group1, title: '4054 group 1') }
-    let(:ticket2)   { create(:ticket, group: group2, title: '4054 group 2') }
+    let(:group_1) { create(:group) }
+    let(:group_2)     { create(:group) }
+    let(:agent_1)     { create(:agent, groups: [group_1]) }
+    let(:agent_2)     { create(:agent, groups: [group_2]) }
+    let(:agent_all)   { create(:agent, groups: [group_1, group_2]) }
+    let(:ticket_1)    { create(:ticket, group: group_1, title: '4054 group 1') }
+    let(:ticket_2)    { create(:ticket, group: group_2, title: '4054 group 2') }
 
     def authenticate
-      agent1 && agent2 && agent_all
-      ticket1 && ticket2
+      agent_1 && agent_2 && agent_all
+      ticket_1 && ticket_2
       agent_all
     end
 
     def check_owner_empty
       expect(page).to have_select('owner_id', text: '-', visible: :all)
-      expect(page).to have_no_select('owner_id', text: agent1.fullname, visible: :all)
-      expect(page).to have_no_select('owner_id', text: agent2.fullname, visible: :all)
+      expect(page).to have_no_select('owner_id', text: agent_1.fullname, visible: :all)
+      expect(page).to have_no_select('owner_id', text: agent_2.fullname, visible: :all)
     end
 
     def click_ticket(ticket)
@@ -296,21 +451,21 @@ RSpec.describe 'Search', type: :system, authenticated: true, searchindex: true d
     end
 
     def check_owner_agent1_shown
-      expect(page).to have_select('owner_id', text: agent1.fullname)
-      expect(page).to have_no_select('owner_id', text: agent2.fullname)
+      expect(page).to have_select('owner_id', text: agent_1.fullname)
+      expect(page).to have_no_select('owner_id', text: agent_2.fullname)
     end
 
     def check_owner_agent2_shown
-      expect(page).to have_no_select('owner_id', text: agent1.fullname)
-      expect(page).to have_select('owner_id', text: agent2.fullname)
+      expect(page).to have_no_select('owner_id', text: agent_1.fullname)
+      expect(page).to have_select('owner_id', text: agent_2.fullname)
     end
 
     def check_owner_field
       check_owner_empty
-      click_ticket(ticket1)
+      click_ticket(ticket_1)
       check_owner_agent1_shown
-      click_ticket(ticket1)
-      click_ticket(ticket2)
+      click_ticket(ticket_1)
+      click_ticket(ticket_2)
       check_owner_agent2_shown
     end