Fixed issue #1930 - bulk action is not executed for a pending-state.

This commit is contained in:
Martin Edenhofer 2018-05-22 00:57:26 +02:00
parent c0e02c3d89
commit 5b0b10ce59
6 changed files with 339 additions and 59 deletions

View file

@ -1162,9 +1162,10 @@ class Table extends App.Controller
# start organization popups # start organization popups
@organizationPopups() @organizationPopups()
@bulkForm = new BulkForm @bulkForm = new BulkForm(
holder: @el holder: @el
view: @view view: @view
)
# start bulk action observ # start bulk action observ
@el.append(@bulkForm.el) @el.append(@bulkForm.el)
@ -1226,12 +1227,16 @@ class BulkForm extends App.Controller
constructor: -> constructor: ->
super super
@configure_attributes_ticket = [ @configure_attributes_ticket = []
{ name: 'state_id', display: 'State', tag: 'select', multiple: false, null: true, relation: 'TicketState', translate: true, nulloption: true, default: '' }, used_attributes = ['state_id', 'pending_time', 'priority_id', 'group_id', 'owner_id']
{ name: 'priority_id', display: 'Priority', tag: 'select', multiple: false, null: true, relation: 'TicketPriority', translate: true, nulloption: true, default: '' }, attributesClean = App.Ticket.attributesGet('edit')
{ name: 'group_id', display: 'Group', tag: 'select', multiple: false, null: true, relation: 'Group', nulloption: true }, for attributeName, attribute of attributesClean
{ name: 'owner_id', display: 'Owner', tag: 'select', multiple: false, null: true, relation: 'User', nulloption: true } if _.contains(used_attributes, attributeName)
] localAttribute = clone(attribute)
localAttribute.nulloption = true
localAttribute.default = ''
localAttribute.null = true
@configure_attributes_ticket.push localAttribute
@holder = @options.holder @holder = @options.holder
@visible = false @visible = false
@ -1246,9 +1251,9 @@ class BulkForm extends App.Controller
App.TicketCreateCollection.unbind(@bindId) App.TicketCreateCollection.unbind(@bindId)
render: -> render: ->
@el.css 'right', App.Utils.getScrollBarWidth() @el.css('right', App.Utils.getScrollBarWidth())
@html App.view('agent_ticket_view/bulk')() @html(App.view('agent_ticket_view/bulk')())
handlers = @Config.get('TicketZoomFormHandler') handlers = @Config.get('TicketZoomFormHandler')
@ -1304,12 +1309,15 @@ class BulkForm extends App.Controller
setTimeout ( => @$('.textarea.form-group textarea').focus() ), 0 setTimeout ( => @$('.textarea.form-group textarea').focus() ), 0
reset: => reset: =>
@$('.js-action-step').removeClass('hide') @cancel()
@$('.js-confirm-step').addClass('hide')
if @visible if @visible
@makeSpaceForTableRows() @makeSpaceForTableRows()
cancel: =>
@$('.js-action-step').removeClass('hide')
@$('.js-confirm-step').addClass('hide')
show: => show: =>
@el.removeClass('hide') @el.removeClass('hide')
@visible = true @visible = true
@ -1325,30 +1333,84 @@ class BulkForm extends App.Controller
scrollParent = @holder.scrollParent() scrollParent = @holder.scrollParent()
isScrolledToBottom = scrollParent.prop('scrollHeight') is scrollParent.scrollTop() + scrollParent.outerHeight() isScrolledToBottom = scrollParent.prop('scrollHeight') is scrollParent.scrollTop() + scrollParent.outerHeight()
@holder.css 'margin-bottom', height @holder.css('margin-bottom', height)
if isScrolledToBottom if isScrolledToBottom
scrollParent.scrollTop scrollParent.prop('scrollHeight') - scrollParent.outerHeight() scrollParent.scrollTop scrollParent.prop('scrollHeight') - scrollParent.outerHeight()
removeSpaceForTableRows: => removeSpaceForTableRows: =>
@holder.css 'margin-bottom', 0 @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) => submit: (e) =>
e.preventDefault() e.preventDefault()
@bulk_count = @holder.find('.table-overview').find('[name="bulk"]:checked').length @bulkCount = @holder.find('.table-overview').find('[name="bulk"]:checked').length
@bulk_count_index = 0
@holder.find('.table-overview').find('[name="bulk"]:checked').each( (index, element) => if @bulkCount is 0
@log 'notice', '@bulk_count_index', @bulk_count, @bulk_count_index 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_id = $(element).val()
ticket = App.Ticket.find(ticket_id) ticket_ids.push ticket_id
)
params = @formParam(e.target) 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 # update ticket
ticket_update = {} ticketUpdate = @ticketMergeParams(params)
for item of params
if params[item] != ''
ticket_update[item] = params[item]
# validate article # validate article
if params['body'] if params['body']
@ -1372,7 +1434,7 @@ class BulkForm extends App.Controller
@formEnable(e) @formEnable(e)
return return
ticket.load(ticket_update) ticket.load(ticketUpdate)
# if title is empty - ticket can't processed, set ? # if title is empty - ticket can't processed, set ?
if _.isEmpty(ticket.title) if _.isEmpty(ticket.title)
@ -1380,7 +1442,7 @@ class BulkForm extends App.Controller
ticket.save( ticket.save(
done: (r) => done: (r) =>
@bulk_count_index++ @bulkCountIndex++
# reset form after save # reset form after save
if article if article
@ -1390,13 +1452,22 @@ class BulkForm extends App.Controller
) )
# refresh view after all tickets are proceeded # refresh view after all tickets are proceeded
if @bulk_count_index == @bulk_count if @bulkCountIndex == @bulkCount
@render()
@hide() @hide()
# fetch overview data again # fetch overview data again
App.Event.trigger('overview:fetch') 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)
}
) )
)
@holder.find('.table-overview').find('[name="bulk"]:checked').prop('checked', false) @holder.find('.table-overview').find('[name="bulk"]:checked').prop('checked', false)
App.Event.trigger 'notify', { App.Event.trigger 'notify', {
type: 'success' type: 'success'

View file

@ -27,6 +27,7 @@
- fix that place method doesn't think that the container is the window, but rather the real window is the window - fix that place method doesn't think that the container is the window, but rather the real window is the window
- added rerender method to show correct today if task is longer open the 24 hours - added rerender method to show correct today if task is longer open the 24 hours
- scroll into view - scroll into view
- fix vertical auto position
*/ */
(function(factory){ (function(factory){
@ -689,7 +690,9 @@
visualPadding = 10, visualPadding = 10,
container = $(this.o.container), container = $(this.o.container),
windowWidth = $(window).width(), windowWidth = $(window).width(),
scrollTop = container.scrollTop(), scrollTop = container.scrollParent().scrollTop(),
bottomEdge = container.offset().top + container.height(),
scrollHeight = container.scrollParent().prop('scrollHeight'),
appendOffset = container.offset(); appendOffset = container.offset();
var parentsZindex = []; var parentsZindex = [];
@ -738,10 +741,10 @@
// auto y orientation is best-situation: top or bottom, no fudging, // auto y orientation is best-situation: top or bottom, no fudging,
// decision based on which shows more of the calendar // decision based on which shows more of the calendar
var yorient = this.o.orientation.y, var yorient = this.o.orientation.y,
top_overflow; space_below;
if (yorient === 'auto'){ if (yorient === 'auto'){
top_overflow = -scrollTop + top - calendarHeight; space_below = scrollHeight - bottomEdge - scrollTop;
yorient = top_overflow < 0 ? 'bottom' : 'top'; yorient = space_below > calendarHeight ? 'bottom' : 'top';
} }
this.picker.addClass('datepicker-orient-' + yorient); this.picker.addClass('datepicker-orient-' + yorient);

View file

@ -1,4 +1,4 @@
<div class="u-positionOrigin" data-name="<%= @attribute.nameRaw %>"> <div class="control controls--date" data-name="<%= @attribute.nameRaw %>">
<input type="hidden" value="<%= @attribute.value %>" name="<%= @attribute.name %>"> <input type="hidden" value="<%= @attribute.value %>" name="<%= @attribute.name %>">
<input type="text" value="" class="form-control js-datepicker <%= @attribute.class %>" data-item="date"> <input type="text" value="" class="form-control js-datepicker <%= @attribute.class %>" data-item="date">
</div> </div>

View file

@ -1,4 +1,4 @@
<div class="horizontal u-positionOrigin" data-name="<%= @attribute.nameRaw %>"> <div class="controls controls--datetime" data-name="<%= @attribute.nameRaw %>">
<input type="hidden" value="<%= @attribute.value %>" name="<%= @attribute.name %>"> <input type="hidden" value="<%= @attribute.value %>" name="<%= @attribute.name %>">
<input type="text" value="" class="form-control flex-shrink-horizontal js-datepicker <%= @attribute.class %>" data-item="date"> <input type="text" value="" class="form-control flex-shrink-horizontal js-datepicker <%= @attribute.class %>" data-item="date">
<div class="controls-label"><%- @T('at') %></div> <div class="controls-label"><%- @T('at') %></div>

View file

@ -1634,6 +1634,15 @@ fieldset > .form-group {
position: relative; position: relative;
} }
.controls--datetime {
position: relative;
display: flex;
}
.controls--date {
position: relative;
}
.controls-label { .controls-label {
margin: 11px 10px 0; margin: 11px 10px 0;
flex-shrink: 0; flex-shrink: 0;
@ -2980,6 +2989,11 @@ footer {
align-items: center; align-items: center;
} }
.bulkAction-firstStep .has-error {
border-color: red !important;
border: 1px solid;
}
.bulkAction-secondStep { .bulkAction-secondStep {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -4570,6 +4584,11 @@ footer {
position: relative; position: relative;
height: 60px; height: 60px;
flex: 1 1 auto; flex: 1 1 auto;
&.datetime {
min-width: 140px;
overflow: visible; // datepicker popup needs to be visible
}
} }
.form-group.is-changed { .form-group.is-changed {
@ -4638,17 +4657,46 @@ footer {
left: 0; left: 0;
top: 0; top: 0;
position: absolute; position: absolute;
padding: 28px 18px 12px; padding: 28px 5px 12px 20px;
float: none; float: none;
display: block; display: block;
border-radius: 0; border-radius: 0;
background: none; background: none;
} }
.form-inline .controls--select { .form-inline {
.controls--datetime,
.controls--date,
.controls--select {
position: static; position: static;
} }
.controls--datetime {
position: absolute;
bottom: 12px;
left: 0;
padding: 0 5px 0 20px;
width: 100%;
.controls-label {
display: none;
}
.form-control {
width: 70px;
line-height: inherit;
position: static;
padding: 0;
height: auto;
&.time {
margin-left: 5px;
width: 38px;
}
}
}
}
.bulkAction-secondStep .form-group { .bulkAction-secondStep .form-group {
min-width: 140px; min-width: 140px;
} }

View file

@ -2,7 +2,7 @@
require 'browser_test_helper' require 'browser_test_helper'
class AgentTicketOverviewLevel0Test < TestCase class AgentTicketOverviewLevel0Test < TestCase
def test_i def test_bulk_close
@browser = browser_instance @browser = browser_instance
login( login(
username: 'master@example.com', username: 'master@example.com',
@ -38,56 +38,63 @@ class AgentTicketOverviewLevel0Test < TestCase
) )
click(text: 'Unassigned & Open') click(text: 'Unassigned & Open')
sleep 8 # till overview is rendered watch_for(
css: '.content.active',
value: 'overview count test #2',
)
# select both via bulk action # select both via bulk action
click( click(
css: '.active table tr td input[value="' + ticket1[:id] + '"] + .icon-checkbox.icon-unchecked', css: '.content.active table tr td input[value="' + ticket1[:id] + '"] + .icon-checkbox.icon-unchecked',
fast: true, fast: true,
) )
# scroll to reply - needed for chrome # scroll to reply - needed for chrome
scroll_to( scroll_to(
position: 'top', position: 'top',
css: '.active table tr td input[value="' + ticket2[:id] + '"] + .icon-checkbox.icon-unchecked', css: '.content.active table tr td input[value="' + ticket2[:id] + '"] + .icon-checkbox.icon-unchecked',
) )
click( click(
css: '.active table tr td input[value="' + ticket2[:id] + '"] + .icon-checkbox.icon-unchecked', css: '.content.active table tr td input[value="' + ticket2[:id] + '"] + .icon-checkbox.icon-unchecked',
fast: true, fast: true,
) )
exists( exists(
css: '.active table tr td input[value="' + ticket1[:id] + '"][type="checkbox"]:checked', css: '.content.active table tr td input[value="' + ticket1[:id] + '"][type="checkbox"]:checked',
) )
exists( exists(
css: '.active table tr td input[value="' + ticket2[:id] + '"][type="checkbox"]:checked', css: '.content.active table tr td input[value="' + ticket2[:id] + '"][type="checkbox"]:checked',
) )
# select close state & submit # select close state & submit
select( select(
css: '.active .bulkAction [name="state_id"]', css: '.content.active .bulkAction [name="state_id"]',
value: 'closed', value: 'closed',
) )
click( click(
css: '.active .bulkAction .js-confirm', css: '.content.active .bulkAction .js-confirm',
) )
click( click(
css: '.active .bulkAction .js-submit', css: '.content.active .bulkAction .js-submit',
)
watch_for_disappear(
css: '.content.active table tr td input[value="' + ticket2[:id] + '"]',
timeout: 6,
) )
sleep 6
exists_not( exists_not(
css: '.active table tr td input[value="' + ticket1[:id] + '"]', css: '.content.active table tr td input[value="' + ticket1[:id] + '"]',
) )
exists_not( exists_not(
css: '.active table tr td input[value="' + ticket2[:id] + '"]', css: '.content.active table tr td input[value="' + ticket2[:id] + '"]',
) )
# remember current overview count # remember current overview count
overview_counter_before = overview_counter() overview_counter_before = overview_counter()
# click options and enable number and article count # click options and enable number and article count
click(css: '.active [data-type="settings"]') click(css: '.content.active [data-type="settings"]')
watch_for( watch_for(
css: '.modal h1', css: '.modal h1',
@ -116,15 +123,15 @@ class AgentTicketOverviewLevel0Test < TestCase
# check if number and article count is shown # check if number and article count is shown
match( match(
css: '.active table th:nth-child(3)', css: '.content.active table th:nth-child(3)',
value: '#', value: '#',
) )
match( match(
css: '.active table th:nth-child(4)', css: '.content.active table th:nth-child(4)',
value: 'Title', value: 'Title',
) )
match( match(
css: '.active table th:nth-child(7)', css: '.content.active table th:nth-child(7)',
value: 'Article#', value: 'Article#',
) )
@ -134,20 +141,20 @@ class AgentTicketOverviewLevel0Test < TestCase
# check if number and article count is shown # check if number and article count is shown
match( match(
css: '.active table th:nth-child(3)', css: '.content.active table th:nth-child(3)',
value: '#', value: '#',
) )
match( match(
css: '.active table th:nth-child(4)', css: '.content.active table th:nth-child(4)',
value: 'Title', value: 'Title',
) )
match( match(
css: '.active table th:nth-child(7)', css: '.content.active table th:nth-child(7)',
value: 'Article#', value: 'Article#',
) )
# disable number and article count # disable number and article count
click(css: '.active [data-type="settings"]') click(css: '.content.active [data-type="settings"]')
watch_for( watch_for(
css: '.modal h1', css: '.modal h1',
@ -164,15 +171,15 @@ class AgentTicketOverviewLevel0Test < TestCase
# check if number and article count is gone # check if number and article count is gone
match_not( match_not(
css: '.active table th:nth-child(3)', css: '.content.active table th:nth-child(3)',
value: '#', value: '#',
) )
match( match(
css: '.active table th:nth-child(3)', css: '.content.active table th:nth-child(3)',
value: 'Title', value: 'Title',
) )
exists_not( exists_not(
css: '.active table th:nth-child(8)', css: '.content.active table th:nth-child(8)',
) )
# create new ticket # create new ticket
@ -211,4 +218,155 @@ class AgentTicketOverviewLevel0Test < TestCase
# cleanup # cleanup
tasks_close_all() tasks_close_all()
end end
def test_bulk_pending
@browser = browser_instance
login(
username: 'master@example.com',
password: 'test',
url: browser_url,
)
tasks_close_all()
# test bulk action
# create new ticket
ticket1 = ticket_create(
data: {
customer: 'nico',
group: 'Users',
title: 'overview count test #3',
body: 'overview count test #3',
}
)
ticket2 = ticket_create(
data: {
customer: 'nico',
group: 'Users',
title: 'overview count test #4',
body: 'overview count test #4',
}
)
click(text: 'Overviews')
# enable full overviews
execute(
js: '$(".content.active .sidebar").css("display", "block")',
)
click(text: 'Unassigned & Open')
watch_for(
css: '.content.active',
value: 'overview count test #4',
timeout: 8,
)
# remember current overview count
overview_counter_before = overview_counter()
# select both via bulk action
click(
css: '.content.active table tr td input[value="' + ticket1[:id] + '"] + .icon-checkbox.icon-unchecked',
fast: true,
)
# scroll to reply - needed for chrome
scroll_to(
position: 'top',
css: '.content.active table tr td input[value="' + ticket2[:id] + '"] + .icon-checkbox.icon-unchecked',
)
click(
css: '.content.active table tr td input[value="' + ticket2[:id] + '"] + .icon-checkbox.icon-unchecked',
fast: true,
)
exists(
css: '.content.active table tr td input[value="' + ticket1[:id] + '"][type="checkbox"]:checked',
)
exists(
css: '.content.active table tr td input[value="' + ticket2[:id] + '"][type="checkbox"]:checked',
)
exists(
displayed: false,
css: '.content.active .bulkAction [data-name="pending_time"]',
)
select(
css: '.content.active .bulkAction [name="state_id"]',
value: 'pending close',
)
exists(
displayed: true,
css: '.content.active .bulkAction [data-name="pending_time"]',
)
set(
css: '.content.active .bulkAction [data-item="date"]',
value: '05/23/2088',
)
select(
css: '.content.active .bulkAction [name="group_id"]',
value: 'Users',
)
select(
css: '.content.active .bulkAction [name="owner_id"]',
value: 'Test Master Agent',
)
click(
css: '.content.active .bulkAction .js-confirm',
)
click(
css: '.content.active .bulkAction .js-submit',
)
watch_for_disappear(
css: '.content.active table tr td input[value="' + ticket2[:id] + '"]',
timeout: 12,
)
exists_not(
css: '.content.active table tr td input[value="' + ticket1[:id] + '"]',
)
exists_not(
css: '.content.active table tr td input[value="' + ticket2[:id] + '"]',
)
# get new overview count
overview_counter_new = overview_counter()
assert_equal(overview_counter_before['#ticket/view/all_unassigned'] - 2, overview_counter_new['#ticket/view/all_unassigned'])
# open ticket by search
ticket_open_by_search(
number: ticket1[:number],
)
sleep 1
# close ticket
ticket_update(
data: {
state: 'closed',
}
)
# open ticket by search
ticket_open_by_search(
number: ticket2[:number],
)
sleep 1
# close ticket
ticket_update(
data: {
state: 'closed',
}
)
# cleanup
tasks_close_all()
end
end end