From db0229e62926db2b2cad8114600c00be991f6648 Mon Sep 17 00:00:00 2001 From: Bola Ahmed Buari Date: Wed, 18 Aug 2021 10:47:40 +0200 Subject: [PATCH] Fixes #445 - Add bulk option to extended search --- .../javascripts/app/controllers/search.coffee | 111 +++++- .../app/controllers/ticket_overview.coffee | 336 +----------------- .../widget/ticket_bulk_form.coffee | 278 +++++++++++++++ .../app/controllers/widget/ticket_list.coffee | 2 + .../app/lib/app_post/global_search.coffee | 2 +- ..._users_for_ticket_selection_methods.coffee | 43 +++ app/assets/stylesheets/zammad.scss | 9 + spec/system/search_spec.rb | 109 +++++- 8 files changed, 558 insertions(+), 332 deletions(-) create mode 100644 app/assets/javascripts/app/controllers/widget/ticket_bulk_form.coffee create mode 100644 app/assets/javascripts/app/lib/mixins/valid_users_for_ticket_selection_methods.coffee diff --git a/app/assets/javascripts/app/controllers/search.coffee b/app/assets/javascripts/app/controllers/search.coffee index b2698c6e8..0cebfb26e 100644 --- a/app/assets/javascripts/app/controllers/search.coffee +++ b/app/assets/javascripts/app/controllers/search.coffee @@ -167,15 +167,110 @@ class App.Search extends App.Controller object = App[model].fullLocal(item.id) list.push object if model is 'Ticket' + + openTicket = (id,e) => + # open ticket via task manager to provide task with overview info + ticket = App.Ticket.findNative(id) + App.TaskManager.execute( + key: "Ticket-#{ticket.id}" + controller: 'TicketZoom' + params: + ticket_id: ticket.id + overview_id: @overview.id + show: true + ) + @navigate ticket.uiUrl() + + checkbox = @permissionCheck('ticket.agent') ? true : false + + callbackCheckbox = (id, checked, e) => + if @shouldShowBulkForm() + @bulkForm.render() + @bulkForm.show() + else + @bulkForm.hide() + + if @lastChecked && e.shiftKey + # check items in a row + currentItem = $(e.currentTarget).parents('.item') + lastCheckedItem = $(@lastChecked).parents('.item') + items = currentItem.parent().children() + + if currentItem.index() > lastCheckedItem.index() + # current item is below last checked item + startId = lastCheckedItem.index() + endId = currentItem.index() + else + # current item is above last checked item + startId = currentItem.index() + endId = lastCheckedItem.index() + + items.slice(startId+1, endId).find('[name="bulk"]').prop('checked', (-> !@checked)) + + @lastChecked = e.currentTarget + ticket_ids = [] for item in localList ticket_ids.push item.id + localeEl = @$('.js-content') @table = new App.TicketList( - tableId: "find_#{model}" - el: @$('.js-content') + tableId: "find_#{model}" + el: localeEl columns: [ 'number', 'title', 'customer', 'group', 'owner', 'created_at' ] ticket_ids: ticket_ids radio: false + checkbox: checkbox + bindRow: + events: + 'click': openTicket + bindCheckbox: + events: + 'click': callbackCheckbox + select_all: callbackCheckbox + ) + + updateSearch = => + callback = => + @search(true) + @delay(callback, 100) + + @bulkForm = new App.TicketBulkForm( + holder: localeEl + view: @view + callback: updateSearch + noSidebar: true + ) + + # start bulk action observ + @el.append(@bulkForm.el) + localElement = @$('.js-content') + if localElement.find('input[name="bulk"]:checked').length isnt 0 + @bulkForm.show() + + # show/hide bulk action + localElement.delegate('input[name="bulk"], input[name="bulk_all"]', 'change', (e) => + if @shouldShowBulkForm() + @bulkForm.show() + else + @bulkForm.hide() + @bulkForm.reset() + ) + + # deselect bulk_all if one item is uncheck observ + localElement.delegate('[name="bulk"]', 'change', (e) -> + bulkAll = localElement.find('[name="bulk_all"]') + checkedCount = localElement.find('input[name="bulk"]:checked').length + checkboxCount = localElement.find('input[name="bulk"]').length + if checkedCount is 0 + bulkAll.prop('indeterminate', false) + bulkAll.prop('checked', false) + else + if checkedCount is checkboxCount + bulkAll.prop('indeterminate', false) + bulkAll.prop('checked', true) + else + bulkAll.prop('checked', false) + bulkAll.prop('indeterminate', true) ) else openObject = (id,e) => @@ -202,6 +297,18 @@ class App.Search extends App.Controller updateFilledClass: -> @searchInput.toggleClass 'is-empty', !@searchInput.val() + shouldShowBulkForm: => + items = @$('table').find('input[name="bulk"]:checked') + return false if items.length == 0 + + ticket_ids = _.map(items, (el) -> $(el).val() ) + ticket_group_ids = _.map(App.Ticket.findAll(ticket_ids), (ticket) -> ticket.group_id) + ticket_group_ids = _.uniq(ticket_group_ids) + allowed_group_ids = App.User.find(@Session.get('id')).allGroupIds('change') + allowed_group_ids = _.map(allowed_group_ids, (id_string) -> parseInt(id_string, 10) ) + _.every(ticket_group_ids, (id) -> id in allowed_group_ids) + + class Router extends App.ControllerPermanent constructor: (params) -> super diff --git a/app/assets/javascripts/app/controllers/ticket_overview.coffee b/app/assets/javascripts/app/controllers/ticket_overview.coffee index c545020d1..89f391ec7 100644 --- a/app/assets/javascripts/app/controllers/ticket_overview.coffee +++ b/app/assets/javascripts/app/controllers/ticket_overview.coffee @@ -1,47 +1,3 @@ -ValidUsersForTicketSelectionMethods = - validUsersForTicketSelection: -> - items = $('.content.active .table-overview .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 - # after we got those we need to check which users are available in all groups - # users that are not in all groups can't get the tickets assigned - ticket_ids = _.map(items, (el) -> $(el).val() ) - ticket_group_ids = _.map(App.Ticket.findAll(ticket_ids), (ticket) -> ticket.group_id) - users = @usersInGroups(ticket_group_ids) - - # get the list of possible groups for the current user - # from the TicketCreateCollection - # (filled for e.g. the TicketCreation or TicketZoom assignment) - # and order them by name - 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 TicketCreateCollection - # (filled for e.g. the TicketCreation or TicketZoom assignment) - for group in groups - group.valid_users_count = @formMeta?.dependencies?.group_id?[group.id]?.owner_id.length || 0 - - { - users: users - groups: groups_sorted - } - - usersInGroups: (group_ids) -> - ids_by_group = _.chain(@formMeta?.dependencies?.group_id) - .pick(group_ids) - .values() - .map( (e) -> e.owner_id) - .value() - - # Underscore's intersection doesn't work when chained - ids_in_all_groups = _.intersection(ids_by_group...) - - users = App.User.findAll(ids_in_all_groups) - _.sortBy(users, (user) -> user.firstname) - class App.TicketOverview extends App.Controller className: 'overviews' activeFocus: 'nav' @@ -69,7 +25,7 @@ class App.TicketOverview extends App.Controller 'mouseenter .js-batch-hover-target': 'highlightBatchEntry' 'mouseleave .js-batch-hover-target': 'unhighlightBatchEntry' - @include ValidUsersForTicketSelectionMethods + @include App.ValidUsersForTicketSelectionMethods constructor: -> super @@ -1280,18 +1236,19 @@ class Table extends App.Controller @renderPopovers() - @bulkForm = new BulkForm( + @bulkForm = new App.TicketBulkForm( holder: @el view: @view ) # start bulk action observ @el.append(@bulkForm.el) - if @$('.table-overview').find('input[name="bulk"]:checked').length isnt 0 + localElement = @$('.table-overview') + if localElement.find('input[name="bulk"]:checked').length isnt 0 @bulkForm.show() # show/hide bulk action - @$('.table-overview').delegate('input[name="bulk"], input[name="bulk_all"]', 'change', (e) => + localElement.delegate('input[name="bulk"], input[name="bulk_all"]', 'change', (e) => if @shouldShowBulkForm() @bulkForm.show() else @@ -1300,10 +1257,10 @@ class Table extends App.Controller ) # deselect bulk_all if one item is uncheck observ - @$('.table-overview').delegate('[name="bulk"]', 'change', (e) => - bulkAll = @$('.table-overview').find('[name="bulk_all"]') - checkedCount = @$('.table-overview').find('input[name="bulk"]:checked').length - checkboxCount = @$('.table-overview').find('input[name="bulk"]').length + localElement.delegate('[name="bulk"]', 'change', (e) -> + bulkAll = localElement.find('[name="bulk_all"]') + checkedCount = localElement.find('input[name="bulk"]:checked').length + checkboxCount = localElement.find('input[name="bulk"]').length if checkedCount is 0 bulkAll.prop('indeterminate', false) bulkAll.prop('checked', false) @@ -1362,281 +1319,6 @@ class Table extends App.Controller onCloseCallback: @keyboardOn ) -class BulkForm extends App.Controller - className: 'bulkAction hide' - - events: - 'submit form': 'submit' - 'click .js-submit': 'submit' - 'click .js-confirm': 'confirm' - 'click .js-cancel': 'reset' - - @include ValidUsersForTicketSelectionMethods - - constructor: -> - super - - @configure_attributes_ticket = [] - used_attributes = ['state_id', 'pending_time', 'priority_id', 'group_id', 'owner_id'] - attributesClean = App.Ticket.attributesGet('edit') - for attributeName, attribute of attributesClean - if _.contains(used_attributes, attributeName) - localAttribute = clone(attribute) - localAttribute.nulloption = true - localAttribute.default = '' - localAttribute.null = true - @configure_attributes_ticket.push localAttribute - - time_attribute = _.findWhere(@configure_attributes_ticket, {'name': 'pending_time'}) - if time_attribute - time_attribute.orientation = 'top' - time_attribute.disableScroll = true - - @holder = @options.holder - @visible = false - - load = (data) => - App.Collection.loadAssets(data.assets) - @formMeta = data.form_meta - @render() - @bindId = App.TicketCreateCollection.bind(load) - - release: => - App.TicketCreateCollection.unbind(@bindId) - - render: -> - @el.css('right', App.Utils.getScrollBarWidth()) - - @html(App.view('agent_ticket_view/bulk')()) - - handlers = @Config.get('TicketZoomFormHandler') - - for attribute in @configure_attributes_ticket - continue if attribute.name != 'owner_id' - {users, groups} = @validUsersForTicketSelection() - options = _.map(users, (user) -> {value: user.id, name: user.displayName()} ) - attribute.possible_groups_owners = options - - new App.ControllerForm( - el: @$('#form-ticket-bulk') - model: - configure_attributes: @configure_attributes_ticket - className: 'create' - labelClass: 'input-group-addon' - handlersConfig: handlers - params: {} - filter: @formMeta.filter - formMeta: @formMeta - noFieldset: true - ) - - new App.ControllerForm( - el: @$('#form-ticket-bulk-comment') - model: - configure_attributes: [{ name: 'body', display: 'Comment', tag: 'textarea', rows: 4, null: true, upload: false, item_class: 'flex' }] - className: 'create' - labelClass: 'input-group-addon' - noFieldset: true - ) - - @confirm_attributes = [ - { name: 'type_id', display: 'Type', tag: 'select', multiple: false, null: true, relation: 'TicketArticleType', filter: @articleTypeFilter, default: '9', translate: true, class: 'medium' } - { name: 'internal', display: 'Visibility', tag: 'select', null: true, options: { true: 'internal', false: 'public' }, class: 'medium', item_class: '', default: false } - ] - - new App.ControllerForm( - el: @$('#form-ticket-bulk-typeVisibility') - model: - configure_attributes: @confirm_attributes - className: 'create' - labelClass: 'input-group-addon' - noFieldset: true - ) - - articleTypeFilter: (items) -> - for item in items - if item.name is 'note' - return [item] - items - - confirm: => - @$('.js-action-step').addClass('hide') - @$('.js-confirm-step').removeClass('hide') - - @makeSpaceForTableRows() - - # need a delay because of the click event - setTimeout ( => @$('.textarea.form-group textarea').focus() ), 0 - - reset: => - @cancel() - - if @visible - @makeSpaceForTableRows() - - cancel: => - @$('.js-action-step').removeClass('hide') - @$('.js-confirm-step').addClass('hide') - - show: => - @el.removeClass('hide') - @visible = true - @makeSpaceForTableRows() - - hide: => - @el.addClass('hide') - @visible = false - @removeSpaceForTableRows() - - makeSpaceForTableRows: => - height = @el.height() - scrollParent = @holder.scrollParent() - isScrolledToBottom = scrollParent.prop('scrollHeight') is scrollParent.scrollTop() + scrollParent.outerHeight() - - @holder.css('margin-bottom', height) - - if isScrolledToBottom - scrollParent.scrollTop scrollParent.prop('scrollHeight') - scrollParent.outerHeight() - - removeSpaceForTableRows: => - @holder.css('margin-bottom', 0) - - ticketMergeParams: (params) -> - ticketUpdate = {} - for item of params - if params[item] != '' && params[item] != null - ticketUpdate[item] = params[item] - - # in case if a group is selected, set also the selected owner (maybe nobody) - if params.group_id != '' && params.group_id != null - ticketUpdate.owner_id = params.owner_id - ticketUpdate - - submit: (e) => - e.preventDefault() - - @bulkCount = @holder.find('.table-overview').find('[name="bulk"]:checked').length - - if @bulkCount is 0 - App.Event.trigger('notify', { - type: 'error' - msg: App.i18n.translateContent('At least one object must be selected.') - }) - return - - ticket_ids = [] - @holder.find('.table-overview').find('[name="bulk"]:checked').each( (index, element) -> - ticket_id = $(element).val() - ticket_ids.push ticket_id - ) - - params = @formParam(e.target) - - for ticket_id in ticket_ids - ticket = App.Ticket.find(ticket_id) - - ticketUpdate = @ticketMergeParams(params) - ticket.load(ticketUpdate) - - # if title is empty - ticket can't processed, set ? - if _.isEmpty(ticket.title) - ticket.title = '-' - - # validate ticket - errors = ticket.validate( - screen: 'edit' - ) - if errors - @log 'error', 'update', errors - errorString = '' - for key, error of errors - errorString += "#{key}: #{error}" - - @formValidate( - form: e.target - errors: errors - screen: 'edit' - ) - - App.Event.trigger('notify', { - type: 'error' - msg: App.i18n.translateContent('Bulk action stopped %s!', errorString) - }) - @cancel() - return - - @bulkCountIndex = 0 - for ticket_id in ticket_ids - ticket = App.Ticket.find(ticket_id) - - # update ticket - ticketUpdate = @ticketMergeParams(params) - - # validate article - if params['body'] - article = new App.TicketArticle - params.from = @Session.get().displayName() - params.ticket_id = ticket.id - params.form_id = @form_id - - sender = App.TicketArticleSender.findByAttribute('name', 'Agent') - type = App.TicketArticleType.find(params['type_id']) - params.sender_id = sender.id - - if !params['internal'] - params['internal'] = false - - @log 'notice', 'update article', params, sender - article.load(params) - errors = article.validate() - if errors - @log 'error', 'update article', errors - @formEnable(e) - return - - ticket.load(ticketUpdate) - - # if title is empty - ticket can't processed, set ? - if _.isEmpty(ticket.title) - ticket.title = '-' - - @saveTicketArticle(ticket, article) - - @holder.find('.table-overview').find('[name="bulk"]:checked').prop('checked', false) - App.Event.trigger('notify', { - type: 'success' - msg: App.i18n.translateContent('Bulk action executed!') - }) - - saveTicketArticle: (ticket, article) => - ticket.save( - done: (r) => - @bulkCountIndex++ - - # reset form after save - if article - article.save( - fail: (r) => - @log 'error', 'update article', r - ) - - # refresh view after all tickets are proceeded - if @bulkCountIndex == @bulkCount - @render() - @hide() - - # fetch overview data again - App.Event.trigger('overview:fetch') - - fail: (r) => - @bulkCountIndex++ - @log 'error', 'update ticket', r - App.Event.trigger 'notify', { - type: 'error' - msg: App.i18n.translateContent('Can\'t update Ticket %s!', ticket.number) - } - ) - class App.OverviewSettings extends App.ControllerModal buttonClose: true diff --git a/app/assets/javascripts/app/controllers/widget/ticket_bulk_form.coffee b/app/assets/javascripts/app/controllers/widget/ticket_bulk_form.coffee new file mode 100644 index 000000000..befe4da90 --- /dev/null +++ b/app/assets/javascripts/app/controllers/widget/ticket_bulk_form.coffee @@ -0,0 +1,278 @@ +class App.TicketBulkForm extends App.Controller + className: 'bulkAction hide' + + events: + 'submit form': 'submit' + 'click .js-submit': 'submit' + 'click .js-confirm': 'confirm' + 'click .js-cancel': 'reset' + + @include App.ValidUsersForTicketSelectionMethods + + constructor: -> + super + + @configure_attributes_ticket = [] + + used_attributes = ['state_id', 'pending_time', 'priority_id', 'group_id', 'owner_id'] + attributesClean = App.Ticket.attributesGet('edit') + for attributeName, attribute of attributesClean + if _.contains(used_attributes, attributeName) + localAttribute = clone(attribute) + localAttribute.nulloption = true + localAttribute.default = '' + localAttribute.null = true + @configure_attributes_ticket.push localAttribute + + + time_attribute = _.findWhere(@configure_attributes_ticket, {'name': 'pending_time'}) + if time_attribute + time_attribute.orientation = 'top' + time_attribute.disableScroll = true + + @holder = @options.holder + @visible = false + + load = (data) => + App.Collection.loadAssets(data.assets) + @formMeta = data.form_meta + @render() + @bindId = App.TicketCreateCollection.bind(load) + + release: => + App.TicketCreateCollection.unbind(@bindId) + + render: -> + @el.css('right', App.Utils.getScrollBarWidth()) + @el.addClass('no-sidebar') if @noSidebar + + @html(App.view('agent_ticket_view/bulk')()) + + handlers = @Config.get('TicketZoomFormHandler') + + for attribute in @configure_attributes_ticket + continue if attribute.name != 'owner_id' + {users, groups} = @validUsersForTicketSelection() + options = _.map(users, (user) -> {value: user.id, name: user.displayName()} ) + attribute.possible_groups_owners = options + + new App.ControllerForm( + el: @$('#form-ticket-bulk') + model: + configure_attributes: @configure_attributes_ticket + className: 'create' + labelClass: 'input-group-addon' + handlersConfig: handlers + params: {} + filter: @formMeta.filter + formMeta: @formMeta + noFieldset: true + ) + + new App.ControllerForm( + el: @$('#form-ticket-bulk-comment') + model: + configure_attributes: [{ name: 'body', display: 'Comment', tag: 'textarea', rows: 4, null: true, upload: false, item_class: 'flex' }] + className: 'create' + labelClass: 'input-group-addon' + noFieldset: true + ) + + @confirm_attributes = [ + { name: 'type_id', display: 'Type', tag: 'select', multiple: false, null: true, relation: 'TicketArticleType', filter: @articleTypeFilter, default: '9', translate: true, class: 'medium' } + { name: 'internal', display: 'Visibility', tag: 'select', null: true, options: { true: 'internal', false: 'public' }, class: 'medium', item_class: '', default: false } + ] + + new App.ControllerForm( + el: @$('#form-ticket-bulk-typeVisibility') + model: + configure_attributes: @confirm_attributes + className: 'create' + labelClass: 'input-group-addon' + noFieldset: true + ) + + articleTypeFilter: (items) -> + for item in items + if item.name is 'note' + return [item] + items + + confirm: => + @$('.js-action-step').addClass('hide') + @$('.js-confirm-step').removeClass('hide') + + @makeSpaceForTableRows() + + # need a delay because of the click event + setTimeout ( => @$('.textarea.form-group textarea').focus() ), 0 + + reset: => + @cancel() + + if @visible + @makeSpaceForTableRows() + + cancel: => + @$('.js-action-step').removeClass('hide') + @$('.js-confirm-step').addClass('hide') + + show: => + @el.removeClass('hide') + @visible = true + @makeSpaceForTableRows() + + hide: => + @el.addClass('hide') + @visible = false + @removeSpaceForTableRows() + + makeSpaceForTableRows: => + height = @el.height() + scrollParent = @holder.scrollParent() + isScrolledToBottom = scrollParent.prop('scrollHeight') is scrollParent.scrollTop() + scrollParent.outerHeight() + + @holder.css('margin-bottom', height) + + if isScrolledToBottom + scrollParent.scrollTop scrollParent.prop('scrollHeight') - scrollParent.outerHeight() + + removeSpaceForTableRows: => + @holder.css('margin-bottom', 0) + + ticketMergeParams: (params) -> + ticketUpdate = {} + for item of params + if params[item] != '' && params[item] != null + ticketUpdate[item] = params[item] + + # in case if a group is selected, set also the selected owner (maybe nobody) + if params.group_id != '' && params.group_id != null + ticketUpdate.owner_id = params.owner_id + ticketUpdate + + submit: (e) => + e.preventDefault() + + @bulkCount = @holder.find('.table').find('[name="bulk"]:checked').length + + if @bulkCount is 0 + App.Event.trigger('notify', { + type: 'error' + msg: App.i18n.translateContent('At least one object must be selected.') + }) + return + + ticket_ids = [] + @holder.find('.table').find('[name="bulk"]:checked').each( (index, element) -> + ticket_id = $(element).val() + ticket_ids.push ticket_id + ) + + params = @formParam(e.target) + + for ticket_id in ticket_ids + ticket = App.Ticket.find(ticket_id) + + ticketUpdate = @ticketMergeParams(params) + ticket.load(ticketUpdate) + + # if title is empty - ticket can't processed, set ? + if _.isEmpty(ticket.title) + ticket.title = '-' + + # validate ticket + errors = ticket.validate( + screen: 'edit' + ) + if errors + @log 'error', 'update', errors + errorString = '' + for key, error of errors + errorString += "#{key}: #{error}" + + @formValidate( + form: e.target + errors: errors + screen: 'edit' + ) + + App.Event.trigger('notify', { + type: 'error' + msg: App.i18n.translateContent('Bulk action stopped %s!', errorString) + }) + @cancel() + return + + @bulkCountIndex = 0 + for ticket_id in ticket_ids + ticket = App.Ticket.find(ticket_id) + + # update ticket + ticketUpdate = @ticketMergeParams(params) + + # validate article + if params['body'] + article = new App.TicketArticle + params.from = @Session.get().displayName() + params.ticket_id = ticket.id + params.form_id = @form_id + + sender = App.TicketArticleSender.findByAttribute('name', 'Agent') + type = App.TicketArticleType.find(params['type_id']) + params.sender_id = sender.id + + if !params['internal'] + params['internal'] = false + + @log 'notice', 'update article', params, sender + article.load(params) + errors = article.validate() + if errors + @log 'error', 'update article', errors + @formEnable(e) + return + + ticket.load(ticketUpdate) + + # if title is empty - ticket can't processed, set ? + if _.isEmpty(ticket.title) + ticket.title = '-' + + @saveTicketArticle(ticket, article) + + @holder.find('.table').find('[name="bulk"]:checked').prop('checked', false) + App.Event.trigger('notify', { + type: 'success' + msg: App.i18n.translateContent('Bulk action executed!') + }) + + saveTicketArticle: (ticket, article) => + ticket.save( + done: (r) => + @bulkCountIndex++ + + # reset form after save + if article + article.save( + fail: (r) => + @log 'error', 'update article', r + ) + + # refresh view after all tickets are proceeded + if @bulkCountIndex == @bulkCount + @render() + @hide() + + # fetch overview data again + App.Event.trigger('overview:fetch') + + fail: (r) => + @bulkCountIndex++ + @log 'error', 'update ticket', r + App.Event.trigger 'notify', { + type: 'error' + msg: App.i18n.translateContent('Can\'t update Ticket %s!', ticket.number) + } + ) + diff --git a/app/assets/javascripts/app/controllers/widget/ticket_list.coffee b/app/assets/javascripts/app/controllers/widget/ticket_list.coffee index 82c6b4b79..28fb33fc7 100644 --- a/app/assets/javascripts/app/controllers/widget/ticket_list.coffee +++ b/app/assets/javascripts/app/controllers/widget/ticket_list.coffee @@ -117,11 +117,13 @@ class App.TicketList extends App.Controller overview: @columns || [ 'number', 'title', 'customer', 'group', 'created_at' ] model: App.Ticket objects: list + checkbox: @checkbox #bindRow: # events: # 'click': openTicket callbackHeader: callbackHeader callbackAttributes: callbackAttributes + bindCheckbox: @bindCheckbox radio: @radio ) diff --git a/app/assets/javascripts/app/lib/app_post/global_search.coffee b/app/assets/javascripts/app/lib/app_post/global_search.coffee index 0ce040a4d..7199b3a5b 100644 --- a/app/assets/javascripts/app/lib/app_post/global_search.coffee +++ b/app/assets/javascripts/app/lib/app_post/global_search.coffee @@ -11,7 +11,7 @@ class App.GlobalSearch extends App.Controller query = params.query # use cache for search result currentTime = new Date - if @searchResultCache[query] && @searchResultCache[query].time > currentTime.setSeconds(currentTime.getSeconds() - 20) + if !params.force && @searchResultCache[query] && @searchResultCache[query].time > currentTime.setSeconds(currentTime.getSeconds() - 20) if @ajaxRequestId App.Ajax.abort(@ajaxRequestId) @ajaxStart(params) diff --git a/app/assets/javascripts/app/lib/mixins/valid_users_for_ticket_selection_methods.coffee b/app/assets/javascripts/app/lib/mixins/valid_users_for_ticket_selection_methods.coffee new file mode 100644 index 000000000..9a828a4ec --- /dev/null +++ b/app/assets/javascripts/app/lib/mixins/valid_users_for_ticket_selection_methods.coffee @@ -0,0 +1,43 @@ +App.ValidUsersForTicketSelectionMethods = + validUsersForTicketSelection: -> + items = $('.content.active .table-overview .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 + # after we got those we need to check which users are available in all groups + # users that are not in all groups can't get the tickets assigned + ticket_ids = _.map(items, (el) -> $(el).val() ) + ticket_group_ids = _.map(App.Ticket.findAll(ticket_ids), (ticket) -> ticket.group_id) + users = @usersInGroups(ticket_group_ids) + + # get the list of possible groups for the current user + # from the TicketCreateCollection + # (filled for e.g. the TicketCreation or TicketZoom assignment) + # and order them by name + 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 TicketCreateCollection + # (filled for e.g. the TicketCreation or TicketZoom assignment) + for group in groups + group.valid_users_count = @formMeta?.dependencies?.group_id?[group.id]?.owner_id.length || 0 + + { + users: users + groups: groups_sorted + } + + usersInGroups: (group_ids) -> + ids_by_group = _.chain(@formMeta?.dependencies?.group_id) + .pick(group_ids) + .values() + .map( (e) -> e.owner_id) + .value() + + # Underscore's intersection doesn't work when chained + ids_in_all_groups = _.intersection(ids_by_group...) + + users = App.User.findAll(ids_in_all_groups) + _.sortBy(users, (user) -> user.firstname) diff --git a/app/assets/stylesheets/zammad.scss b/app/assets/stylesheets/zammad.scss index cacee02e8..57edf99b9 100644 --- a/app/assets/stylesheets/zammad.scss +++ b/app/assets/stylesheets/zammad.scss @@ -3835,6 +3835,15 @@ footer { min-width: $minWidth - $sidebarWidth; } + &.no-sidebar { + @include bidi-style(left, $navigationWidth, right, 0); + min-width: $minWidth - $navigationWidth; + + @include small-desktop { + min-width: $minWidth; + } + } + @include phone { @include bidi-style(left, $mobileNavigationWidth, right, 0); min-width: 0; diff --git a/spec/system/search_spec.rb b/spec/system/search_spec.rb index ef19e25b0..5d3c7d6d4 100644 --- a/spec/system/search_spec.rb +++ b/spec/system/search_spec.rb @@ -2,7 +2,12 @@ require 'rails_helper' -RSpec.describe 'Search', type: :system, searchindex: true do +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' } + before do configure_elasticsearch(required: true, rebuild: true) end @@ -13,7 +18,107 @@ RSpec.describe 'Search', type: :system, searchindex: true do click_on 'Show Search Details' within '#navigation .tasks a[data-key=Search]' do - expect(page).to have_text '"Welcome"' + expect(page).to have_content '"Welcome"' + end + end + + context 'with ticket search result' do + before do + ticket_1 && ticket_2 && rebuild_searchindex + + fill_in id: 'global-search', with: 'Testing' + click_on 'Show Search Details' + + find('[data-tab-content=Ticket]').click + end + + context 'checkbox' do + it 'has checkbox for each ticket records' do + within '.detail-search table.table' do + expect(page).to have_xpath(".//td[contains(@class, 'js-checkbox-field')]//input[@type='checkbox']", visible: :all, minimum: 2) + end + end + + it 'has select all checkbox' do + within '.detail-search table.table' do + expect(page).to have_xpath(".//th//input[@type='checkbox' and @name='bulk_all']", visible: :all, count: 1) + end + end + + it 'shows bulkform when checkbox is checked' do + within '.detail-search table.table' do + find("tr[data-id='#{ticket_1.id}']").check('bulk', allow_label_click: true) + end + + expect(page).to have_selector('.bulkAction.no-sidebar') + .and have_no_selector('.bulkAction.no-sidebar.hide', visible: :all) + end + + it 'shows bulkform when all checkbox is checked' do + within '.detail-search table.table' do + find('th.table-checkbox').check('bulk_all', allow_label_click: true) + end + + expect(page).to have_selector('.bulkAction.no-sidebar') + .and have_no_selector('.bulkAction.no-sidebar.hide', visible: :all) + end + + it 'hides bulkform when checkbox is unchecked' do + within '.detail-search table.table' do + find('th.table-checkbox').check('bulk_all', allow_label_click: true) + + all('.js-tableBody tr.item').each { |row| row.uncheck('bulk', allow_label_click: true) } + end + + expect(page).to have_selector('.bulkAction.no-sidebar.hide', visible: :hide) + end + end + + context 'with bulkform activated' do + before do + find('th.table-checkbox').check('bulk_all', allow_label_click: true) + end + + it 'has group label' do + within '.bulkAction .bulkAction-form' do + expect(page).to have_content 'GROUP' + end + end + + it 'has owner label' do + within '.bulkAction .bulkAction-form' do + expect(page).to have_content 'OWNER' + end + end + + it 'has state label' do + within '.bulkAction .bulkAction-form' do + expect(page).to have_content 'STATE' + end + end + + it 'has priority label' do + within '.bulkAction .bulkAction-form' do + expect(page).to have_content 'PRIORITY' + end + end + end + + context 'bulk note' do + before { current_window.resize_to(1300, 1040) } + + it 'adds note to selected ticket' do + within :active_content do + find("tr[data-id='#{ticket_1.id}']").check('bulk', allow_label_click: true) + click '.js-confirm' + find('.js-confirm-step textarea').fill_in with: note + click '.js-submit' + end + + expect do + wait(10, interval: 0.1).until { ticket_1.articles.last&.body == note } + end.not_to raise_error + end end end