Init version of scheduler.
This commit is contained in:
parent
32d404def9
commit
51d485670e
15 changed files with 687 additions and 159 deletions
|
@ -923,18 +923,286 @@ class App.ControllerForm extends App.Controller
|
||||||
item.find( "[name=\"#{attribute.name}::count\"]").find("option[value=\"#{attribute.value.count}\"]").attr( 'selected', 'selected' )
|
item.find( "[name=\"#{attribute.name}::count\"]").find("option[value=\"#{attribute.value.count}\"]").attr( 'selected', 'selected' )
|
||||||
item.find( "[name=\"#{attribute.name}::area\"]").find("option[value=\"#{attribute.value.area}\"]").attr( 'selected', 'selected' )
|
item.find( "[name=\"#{attribute.name}::area\"]").find("option[value=\"#{attribute.value.area}\"]").attr( 'selected', 'selected' )
|
||||||
|
|
||||||
# ticket attribute selection
|
# ticket attribute set
|
||||||
else if attribute.tag is 'ticket_attribute_selection'
|
else if attribute.tag is 'ticket_attribute_set'
|
||||||
|
|
||||||
# list of possible attributes
|
# list of possible attributes
|
||||||
item = $(
|
item = $(
|
||||||
App.view('generic/ticket_attribute_selection')(
|
App.view('generic/ticket_attribute_manage')(
|
||||||
|
attribute: attribute
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
addShownAttribute = ( key, value ) =>
|
||||||
|
parts = key.split(/::/)
|
||||||
|
key = parts[0]
|
||||||
|
type = parts[1]
|
||||||
|
if key is 'tickets.title'
|
||||||
|
attribute_config = {
|
||||||
|
name: attribute.name + '::tickets.title'
|
||||||
|
display: 'Title'
|
||||||
|
tag: 'input'
|
||||||
|
type: 'text'
|
||||||
|
null: false
|
||||||
|
value: value
|
||||||
|
remove: true
|
||||||
|
}
|
||||||
|
else if key is 'tickets.group_id'
|
||||||
|
attribute_config = {
|
||||||
|
name: attribute.name + '::tickets.group_id'
|
||||||
|
display: 'Group'
|
||||||
|
tag: 'select'
|
||||||
|
multiple: false
|
||||||
|
null: false
|
||||||
|
nulloption: false
|
||||||
|
relation: 'Group'
|
||||||
|
value: value
|
||||||
|
remove: true
|
||||||
|
}
|
||||||
|
else if key is 'tickets.owner_id' || key is 'tickets.customer_id'
|
||||||
|
display = 'Owner'
|
||||||
|
name = 'owner_id'
|
||||||
|
if key is 'customer_id'
|
||||||
|
display = 'Customer'
|
||||||
|
name = 'customer_id'
|
||||||
|
attribute_config = {
|
||||||
|
name: attribute.name + '::tickets.' + name
|
||||||
|
display: display
|
||||||
|
tag: 'select'
|
||||||
|
multiple: false
|
||||||
|
null: false
|
||||||
|
nulloption: false
|
||||||
|
relation: 'User'
|
||||||
|
value: value || null
|
||||||
|
remove: true
|
||||||
|
filter: ( all, type ) ->
|
||||||
|
return all if type isnt 'collection'
|
||||||
|
all = _.filter( all, (item) ->
|
||||||
|
return if item.id is 1
|
||||||
|
return item
|
||||||
|
)
|
||||||
|
all.unshift( {
|
||||||
|
id: ''
|
||||||
|
name: '--'
|
||||||
|
} )
|
||||||
|
all.unshift( {
|
||||||
|
id: 1
|
||||||
|
name: '*** not set ***'
|
||||||
|
} )
|
||||||
|
all.unshift( {
|
||||||
|
id: 'current_user.id'
|
||||||
|
name: '*** current user ***'
|
||||||
|
} )
|
||||||
|
all
|
||||||
|
}
|
||||||
|
else if key is 'tickets.organization_id'
|
||||||
|
attribute_config = {
|
||||||
|
name: attribute.name + '::tickets.organization_id'
|
||||||
|
display: 'Organization'
|
||||||
|
tag: 'select'
|
||||||
|
multiple: false
|
||||||
|
null: false
|
||||||
|
nulloption: false
|
||||||
|
relation: 'Organization'
|
||||||
|
value: value || null
|
||||||
|
remove: true
|
||||||
|
filter: ( all, type ) ->
|
||||||
|
return all if type isnt 'collection'
|
||||||
|
all.unshift( {
|
||||||
|
id: ''
|
||||||
|
name: '--'
|
||||||
|
} )
|
||||||
|
all.unshift( {
|
||||||
|
id: 'current_user.organization_id'
|
||||||
|
name: '*** organization of current user ***'
|
||||||
|
} )
|
||||||
|
all
|
||||||
|
}
|
||||||
|
else if key is 'tickets.state_id'
|
||||||
|
attribute_config = {
|
||||||
|
name: attribute.name + '::tickets.state_id'
|
||||||
|
display: 'State'
|
||||||
|
tag: 'select'
|
||||||
|
multiple: false
|
||||||
|
null: false
|
||||||
|
nulloption: false
|
||||||
|
relation: 'TicketState'
|
||||||
|
value: value
|
||||||
|
translate: true
|
||||||
|
remove: true
|
||||||
|
}
|
||||||
|
else if key is 'tickets.priority_id'
|
||||||
|
attribute_config = {
|
||||||
|
name: attribute.name + '::tickets.priority_id'
|
||||||
|
display: 'Priority'
|
||||||
|
tag: 'select'
|
||||||
|
multiple: false
|
||||||
|
null: false
|
||||||
|
nulloption: false
|
||||||
|
relation: 'TicketPriority'
|
||||||
|
value: value
|
||||||
|
translate: true
|
||||||
|
remove: true
|
||||||
|
}
|
||||||
|
else
|
||||||
|
attribute_config = {
|
||||||
|
name: attribute.name + '::' + key
|
||||||
|
display: 'FIXME!'
|
||||||
|
tag: 'input'
|
||||||
|
type: 'text'
|
||||||
|
value: value
|
||||||
|
remove: true
|
||||||
|
}
|
||||||
|
item.find('select[name=ticket_attribute_list] option[value="' + key + '"]').hide().prop('disabled', true)
|
||||||
|
|
||||||
|
itemSub = @formGenItem( attribute_config )
|
||||||
|
itemSub.find('.glyphicon-minus').bind('click', (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
value = $(e.target).closest('.controls').find('[name]').attr('name')
|
||||||
|
if value
|
||||||
|
value = value.replace("#{attribute.name}::", '')
|
||||||
|
$(e.target).closest('.sub_attribute').find('select[name=ticket_attribute_list] option[value="' + value + '"]').show().prop('disabled', false)
|
||||||
|
$(@).parent().parent().parent().remove()
|
||||||
|
)
|
||||||
|
# itemSub.append('<a href=\"#\" class=\"icon-minus\"></a>')
|
||||||
|
item.find('.ticket_attribute_item').append( itemSub )
|
||||||
|
|
||||||
|
# list of existing attributes
|
||||||
|
attribute_config = {
|
||||||
|
name: 'ticket_attribute_list'
|
||||||
|
display: 'Add Attribute'
|
||||||
|
tag: 'select'
|
||||||
|
multiple: false
|
||||||
|
null: false
|
||||||
|
# nulloption: true
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: ''
|
||||||
|
name: '-- Ticket --'
|
||||||
|
selected: false
|
||||||
|
disable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'tickets.title'
|
||||||
|
name: 'Title'
|
||||||
|
selected: false
|
||||||
|
disable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'tickets.group_id'
|
||||||
|
name: 'Group'
|
||||||
|
selected: false
|
||||||
|
disable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'tickets.state_id'
|
||||||
|
name: 'State'
|
||||||
|
selected: false
|
||||||
|
disable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'tickets.priority_id'
|
||||||
|
name: 'Priority'
|
||||||
|
selected: true
|
||||||
|
disable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'tickets.owner_id'
|
||||||
|
name: 'Owner'
|
||||||
|
selected: true
|
||||||
|
disable: false
|
||||||
|
},
|
||||||
|
# # {
|
||||||
|
# value: 'tag'
|
||||||
|
# name: 'Tag'
|
||||||
|
# selected: true
|
||||||
|
# disable: false
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# value: '-a'
|
||||||
|
# name: '-- ' + App.i18n.translateInline('Article') + ' --'
|
||||||
|
# selected: false
|
||||||
|
# disable: true
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# value: 'ticket_articles.from'
|
||||||
|
# name: 'From'
|
||||||
|
# selected: true
|
||||||
|
# disable: false
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# value: 'ticket_articles.to'
|
||||||
|
# name: 'To'
|
||||||
|
# selected: true
|
||||||
|
# disable: false
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# value: 'ticket_articles.cc'
|
||||||
|
# name: 'Cc'
|
||||||
|
# selected: true
|
||||||
|
# disable: false
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# value: 'ticket_articles.subject'
|
||||||
|
# name: 'Subject'
|
||||||
|
# selected: true
|
||||||
|
# disable: false
|
||||||
|
# },
|
||||||
|
# {
|
||||||
|
# value: 'ticket_articles.body'
|
||||||
|
# name: 'Text'
|
||||||
|
# selected: true
|
||||||
|
# disable: false
|
||||||
|
# },
|
||||||
|
{
|
||||||
|
value: '-c'
|
||||||
|
name: '-- ' + App.i18n.translateInline('Customer') + ' --'
|
||||||
|
selected: false
|
||||||
|
disable: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'customers.id'
|
||||||
|
name: 'Customer'
|
||||||
|
selected: true
|
||||||
|
disable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'organization.id'
|
||||||
|
name: 'Organization'
|
||||||
|
selected: true
|
||||||
|
disable: false
|
||||||
|
},
|
||||||
|
]
|
||||||
|
default: ''
|
||||||
|
translate: true
|
||||||
|
class: 'medium'
|
||||||
|
add: true
|
||||||
|
}
|
||||||
|
list = @formGenItem( attribute_config )
|
||||||
|
list.find('.glyphicon-plus').bind('click', (e) ->
|
||||||
|
e.preventDefault()
|
||||||
|
value = $(e.target).closest('.controls').find('[name=ticket_attribute_list]').val()
|
||||||
|
addShownAttribute( value, '' )
|
||||||
|
)
|
||||||
|
item.find('.ticket_attribute_list').prepend( list )
|
||||||
|
|
||||||
|
# list of shown attributes
|
||||||
|
show = []
|
||||||
|
if attribute.value
|
||||||
|
for key, value of attribute.value
|
||||||
|
addShownAttribute( key, value )
|
||||||
|
|
||||||
|
# ticket attribute selection
|
||||||
|
else if attribute.tag is 'ticket_attribute_selection'
|
||||||
|
|
||||||
|
# list of possible attributes
|
||||||
|
item = $(
|
||||||
|
App.view('generic/ticket_attribute_manage')(
|
||||||
attribute: attribute
|
attribute: attribute
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
addShownAttribute = ( key, value ) =>
|
addShownAttribute = ( key, value ) =>
|
||||||
console.log( 'addShownAttribute', key, value )
|
|
||||||
parts = key.split(/::/)
|
parts = key.split(/::/)
|
||||||
key = parts[0]
|
key = parts[0]
|
||||||
type = parts[1]
|
type = parts[1]
|
||||||
|
@ -946,7 +1214,6 @@ class App.ControllerForm extends App.Controller
|
||||||
type: 'text'
|
type: 'text'
|
||||||
null: false
|
null: false
|
||||||
value: value
|
value: value
|
||||||
class: 'medium'
|
|
||||||
remove: true
|
remove: true
|
||||||
}
|
}
|
||||||
else if key is 'tickets.title'
|
else if key is 'tickets.title'
|
||||||
|
@ -957,7 +1224,6 @@ class App.ControllerForm extends App.Controller
|
||||||
type: 'text'
|
type: 'text'
|
||||||
null: false
|
null: false
|
||||||
value: value
|
value: value
|
||||||
class: 'medium'
|
|
||||||
remove: true
|
remove: true
|
||||||
}
|
}
|
||||||
else if key is 'tickets.group_id'
|
else if key is 'tickets.group_id'
|
||||||
|
@ -970,7 +1236,6 @@ class App.ControllerForm extends App.Controller
|
||||||
nulloption: false
|
nulloption: false
|
||||||
relation: 'Group'
|
relation: 'Group'
|
||||||
value: value
|
value: value
|
||||||
class: 'medium'
|
|
||||||
remove: true
|
remove: true
|
||||||
}
|
}
|
||||||
else if key is 'tickets.owner_id' || key is 'tickets.customer_id'
|
else if key is 'tickets.owner_id' || key is 'tickets.customer_id'
|
||||||
|
@ -988,7 +1253,6 @@ class App.ControllerForm extends App.Controller
|
||||||
nulloption: false
|
nulloption: false
|
||||||
relation: 'User'
|
relation: 'User'
|
||||||
value: value || null
|
value: value || null
|
||||||
class: 'medium'
|
|
||||||
remove: true
|
remove: true
|
||||||
filter: ( all, type ) ->
|
filter: ( all, type ) ->
|
||||||
return all if type isnt 'collection'
|
return all if type isnt 'collection'
|
||||||
|
@ -1020,7 +1284,6 @@ class App.ControllerForm extends App.Controller
|
||||||
nulloption: false
|
nulloption: false
|
||||||
relation: 'Organization'
|
relation: 'Organization'
|
||||||
value: value || null
|
value: value || null
|
||||||
class: 'medium'
|
|
||||||
remove: true
|
remove: true
|
||||||
filter: ( all, type ) ->
|
filter: ( all, type ) ->
|
||||||
return all if type isnt 'collection'
|
return all if type isnt 'collection'
|
||||||
|
@ -1045,7 +1308,6 @@ class App.ControllerForm extends App.Controller
|
||||||
relation: 'TicketState'
|
relation: 'TicketState'
|
||||||
value: value
|
value: value
|
||||||
translate: true
|
translate: true
|
||||||
class: 'medium'
|
|
||||||
remove: true
|
remove: true
|
||||||
}
|
}
|
||||||
else if key is 'tickets.priority_id'
|
else if key is 'tickets.priority_id'
|
||||||
|
@ -1059,7 +1321,6 @@ class App.ControllerForm extends App.Controller
|
||||||
relation: 'TicketPriority'
|
relation: 'TicketPriority'
|
||||||
value: value
|
value: value
|
||||||
translate: true
|
translate: true
|
||||||
class: 'medium'
|
|
||||||
remove: true
|
remove: true
|
||||||
}
|
}
|
||||||
else if key is 'tickets.created_at' && ( type is '<>' || value.count )
|
else if key is 'tickets.created_at' && ( type is '<>' || value.count )
|
||||||
|
@ -1069,7 +1330,6 @@ class App.ControllerForm extends App.Controller
|
||||||
tag: 'time_before_last'
|
tag: 'time_before_last'
|
||||||
value: value
|
value: value
|
||||||
translate: true
|
translate: true
|
||||||
class: 'medium'
|
|
||||||
remove: true
|
remove: true
|
||||||
}
|
}
|
||||||
else if key is 'tickets.created_at' && ( type is '><' || 0 )
|
else if key is 'tickets.created_at' && ( type is '><' || 0 )
|
||||||
|
@ -1079,7 +1339,6 @@ class App.ControllerForm extends App.Controller
|
||||||
tag: 'time_range'
|
tag: 'time_range'
|
||||||
value: value
|
value: value
|
||||||
translate: true
|
translate: true
|
||||||
class: 'medium'
|
|
||||||
remove: true
|
remove: true
|
||||||
}
|
}
|
||||||
else if key is 'tickets.close_time' && ( type is '<>' || value.count )
|
else if key is 'tickets.close_time' && ( type is '<>' || value.count )
|
||||||
|
@ -1089,7 +1348,6 @@ class App.ControllerForm extends App.Controller
|
||||||
tag: 'time_before_last'
|
tag: 'time_before_last'
|
||||||
value: value
|
value: value
|
||||||
translate: true
|
translate: true
|
||||||
class: 'medium'
|
|
||||||
remove: true
|
remove: true
|
||||||
}
|
}
|
||||||
else if key is 'tickets.close_time' && ( type is '><' || 0 )
|
else if key is 'tickets.close_time' && ( type is '><' || 0 )
|
||||||
|
@ -1099,7 +1357,6 @@ class App.ControllerForm extends App.Controller
|
||||||
tag: 'time_range'
|
tag: 'time_range'
|
||||||
value: value
|
value: value
|
||||||
translate: true
|
translate: true
|
||||||
class: 'medium'
|
|
||||||
remove: true
|
remove: true
|
||||||
}
|
}
|
||||||
else if key is 'tickets.updated_at' && ( type is '<>' || value.count )
|
else if key is 'tickets.updated_at' && ( type is '<>' || value.count )
|
||||||
|
@ -1109,7 +1366,6 @@ class App.ControllerForm extends App.Controller
|
||||||
tag: 'time_before_last'
|
tag: 'time_before_last'
|
||||||
value: value
|
value: value
|
||||||
translate: true
|
translate: true
|
||||||
class: 'medium'
|
|
||||||
remove: true
|
remove: true
|
||||||
}
|
}
|
||||||
else if key is 'tickets.updated_at' && ( type is '><' || 0 )
|
else if key is 'tickets.updated_at' && ( type is '><' || 0 )
|
||||||
|
@ -1119,7 +1375,6 @@ class App.ControllerForm extends App.Controller
|
||||||
tag: 'time_range'
|
tag: 'time_range'
|
||||||
value: value
|
value: value
|
||||||
translate: true
|
translate: true
|
||||||
class: 'medium'
|
|
||||||
remove: true
|
remove: true
|
||||||
}
|
}
|
||||||
else if key is 'tickets.escalation_time' && ( type is '<>' || value.count )
|
else if key is 'tickets.escalation_time' && ( type is '<>' || value.count )
|
||||||
|
@ -1129,7 +1384,6 @@ class App.ControllerForm extends App.Controller
|
||||||
tag: 'time_before_last'
|
tag: 'time_before_last'
|
||||||
value: value
|
value: value
|
||||||
translate: true
|
translate: true
|
||||||
class: 'medium'
|
|
||||||
remove: true
|
remove: true
|
||||||
}
|
}
|
||||||
else if key is 'tickets.escalation_time' && ( type is '><' || 0 )
|
else if key is 'tickets.escalation_time' && ( type is '><' || 0 )
|
||||||
|
@ -1139,7 +1393,6 @@ class App.ControllerForm extends App.Controller
|
||||||
tag: 'time_range'
|
tag: 'time_range'
|
||||||
value: value
|
value: value
|
||||||
translate: true
|
translate: true
|
||||||
class: 'medium'
|
|
||||||
remove: true
|
remove: true
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -1149,24 +1402,23 @@ class App.ControllerForm extends App.Controller
|
||||||
tag: 'input'
|
tag: 'input'
|
||||||
type: 'text'
|
type: 'text'
|
||||||
value: value
|
value: value
|
||||||
class: 'medium'
|
|
||||||
remove: true
|
remove: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item.find('select[name=ticket_attribute_list] option[value="' + key + '"]').hide().prop('disabled', true)
|
||||||
|
|
||||||
itemSub = @formGenItem( attribute_config )
|
itemSub = @formGenItem( attribute_config )
|
||||||
itemSub.find('.glyphicon-minus').bind('click', (e) ->
|
itemSub.find('.glyphicon-minus').bind('click', (e) ->
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
value = $(e.target).closest('.controls').find('[name]').attr('name')
|
||||||
|
if value
|
||||||
|
value = value.replace("#{attribute.name}::", '')
|
||||||
|
$(e.target).closest('.sub_attribute').find('select[name=ticket_attribute_list] option[value="' + value + '"]').show().prop('disabled', false)
|
||||||
$(@).parent().parent().parent().remove()
|
$(@).parent().parent().parent().remove()
|
||||||
)
|
)
|
||||||
# itemSub.append('<a href=\"#\" class=\"icon-minus\"></a>')
|
# itemSub.append('<a href=\"#\" class=\"icon-minus\"></a>')
|
||||||
item.find('.ticket_attribute_item').append( itemSub )
|
item.find('.ticket_attribute_item').append( itemSub )
|
||||||
|
|
||||||
|
|
||||||
# list of shown attributes
|
|
||||||
show = []
|
|
||||||
if attribute.value
|
|
||||||
for key, value of attribute.value
|
|
||||||
addShownAttribute( key, value )
|
|
||||||
|
|
||||||
# list of existing attributes
|
# list of existing attributes
|
||||||
attribute_config = {
|
attribute_config = {
|
||||||
name: 'ticket_attribute_list'
|
name: 'ticket_attribute_list'
|
||||||
|
@ -1182,19 +1434,18 @@ class App.ControllerForm extends App.Controller
|
||||||
selected: false
|
selected: false
|
||||||
disable: true
|
disable: true
|
||||||
},
|
},
|
||||||
#
|
{
|
||||||
#{
|
value: 'tickets.number'
|
||||||
# value: 'tickets.number'
|
name: 'Number'
|
||||||
# name: 'Number'
|
selected: false
|
||||||
# selected: true
|
disable: false
|
||||||
# disable: false
|
},
|
||||||
#},
|
{
|
||||||
#{
|
value: 'tickets.title'
|
||||||
# value: 'tickets.title'
|
name: 'Title'
|
||||||
# name: 'Title'
|
selected: false
|
||||||
# selected: true
|
disable: false
|
||||||
# disable: false
|
},
|
||||||
#},
|
|
||||||
{
|
{
|
||||||
value: 'tickets.group_id'
|
value: 'tickets.group_id'
|
||||||
name: 'Group'
|
name: 'Group'
|
||||||
|
@ -1373,12 +1624,100 @@ class App.ControllerForm extends App.Controller
|
||||||
|
|
||||||
list.find('.glyphicon-plus').bind('click', (e) ->
|
list.find('.glyphicon-plus').bind('click', (e) ->
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
value = $(e.target).closest('.controls').find('[name=ticket_attribute_list]').val()
|
||||||
value = $(e.target).parents().find('[name=ticket_attribute_list]').val()
|
|
||||||
addShownAttribute( value, '' )
|
addShownAttribute( value, '' )
|
||||||
)
|
)
|
||||||
item.find('.ticket_attribute_list').prepend( list )
|
item.find('.ticket_attribute_list').prepend( list )
|
||||||
|
|
||||||
|
# list of shown attributes
|
||||||
|
show = []
|
||||||
|
if attribute.value
|
||||||
|
for key, value of attribute.value
|
||||||
|
addShownAttribute( key, value )
|
||||||
|
|
||||||
|
# timeplan
|
||||||
|
else if attribute.tag is 'timeplan'
|
||||||
|
item = $( App.view('generic/timeplan')( attribute: attribute ) )
|
||||||
|
attribute_config = {
|
||||||
|
name: "#{attribute.name}::days"
|
||||||
|
tag: 'select'
|
||||||
|
multiple: true
|
||||||
|
null: false
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: 'mon'
|
||||||
|
name: 'Monday'
|
||||||
|
selected: false
|
||||||
|
disable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'tue'
|
||||||
|
name: 'Tuesday'
|
||||||
|
selected: false
|
||||||
|
disable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'wed'
|
||||||
|
name: 'Wednesday'
|
||||||
|
selected: false
|
||||||
|
disable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'thu'
|
||||||
|
name: 'Thursday'
|
||||||
|
selected: false
|
||||||
|
disable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'fri'
|
||||||
|
name: 'Friday'
|
||||||
|
selected: false
|
||||||
|
disable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'sat'
|
||||||
|
name: 'Saturday'
|
||||||
|
selected: false
|
||||||
|
disable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'sun'
|
||||||
|
name: 'Sunday'
|
||||||
|
selected: false
|
||||||
|
disable: false
|
||||||
|
},
|
||||||
|
]
|
||||||
|
default: attribute.default?.days
|
||||||
|
}
|
||||||
|
item.find('.days').append( @formGenItem( attribute_config ) )
|
||||||
|
|
||||||
|
hours = {}
|
||||||
|
for hour in [0..23]
|
||||||
|
localHour = "0#{hour}"
|
||||||
|
hours[hour] = localHour.substr(localHour.length-2,2)
|
||||||
|
attribute_config = {
|
||||||
|
name: "#{attribute.name}::hours"
|
||||||
|
tag: 'select'
|
||||||
|
multiple: true
|
||||||
|
null: false
|
||||||
|
options: hours
|
||||||
|
default: attribute.default?.hours
|
||||||
|
}
|
||||||
|
item.find('.hours').append( @formGenItem( attribute_config ) )
|
||||||
|
|
||||||
|
minutes = {}
|
||||||
|
for minute in [0..5]
|
||||||
|
minutes["#{minute}0"] = "#{minute}0"
|
||||||
|
attribute_config = {
|
||||||
|
name: "#{attribute.name}::minutes"
|
||||||
|
tag: 'select'
|
||||||
|
multiple: true
|
||||||
|
null: false
|
||||||
|
options: minutes
|
||||||
|
default: attribute.default?.miuntes
|
||||||
|
}
|
||||||
|
item.find('.minutes').append( @formGenItem( attribute_config ) )
|
||||||
|
|
||||||
# input
|
# input
|
||||||
else
|
else
|
||||||
item = $( App.view('generic/input')( attribute: attribute ) )
|
item = $( App.view('generic/input')( attribute: attribute ) )
|
||||||
|
|
|
@ -5,17 +5,23 @@ class Index extends App.ControllerContent
|
||||||
# check authentication
|
# check authentication
|
||||||
return if !@authenticate()
|
return if !@authenticate()
|
||||||
|
|
||||||
# set title
|
new App.ControllerGenericIndex(
|
||||||
@title 'Scheduler'
|
el: @el,
|
||||||
@navupdate '#scheduler'
|
id: @id,
|
||||||
|
genericObject: 'Job',
|
||||||
# render page
|
pageData: {
|
||||||
@render()
|
title: 'Schedulers',
|
||||||
|
home: 'schedulers',
|
||||||
render: ->
|
object: 'Scheduler',
|
||||||
|
objects: 'Schedulers',
|
||||||
@html App.view('scheduler')(
|
navupdate: '#schedulers',
|
||||||
head: 'some header'
|
notes: [
|
||||||
|
'Scheduler are ...'
|
||||||
|
],
|
||||||
|
buttons: [
|
||||||
|
{ name: 'New Scheduler', 'data-type': 'new', class: 'btn--success' },
|
||||||
|
],
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
App.Config.set( 'Scheduler', { prio: 2000, name: 'Schedulers', parent: '#manage', target: '#manage/schedulers', controller: Index, role: ['Admin'] }, 'NavBarAdmin' )
|
App.Config.set( 'Scheduler', { prio: 3000, name: 'Schedulers', parent: '#manage', target: '#manage/schedulers', controller: Index, role: ['Admin'] }, 'NavBarAdmin' )
|
|
@ -18,8 +18,4 @@ class Index extends App.ControllerContent
|
||||||
head: 'some header'
|
head: 'some header'
|
||||||
)
|
)
|
||||||
|
|
||||||
#App.Config.set( 'trigger', Index, 'Routes' )
|
App.Config.set( 'Trigger', { prio: 3100, name: 'Triggers', parent: '#manage', target: '#manage/triggers', controller: Index, role: ['Admin'] }, 'NavBarAdmin' )
|
||||||
#App.Config.set( 'Trigger', { prio: 3000, parent: '#admin', name: 'Trigger', target: '#trigger', role: ['Admin'] }, 'NavBar' )
|
|
||||||
|
|
||||||
App.Config.set( 'Trigger', { prio: 3000, name: 'Triggers', target: '#manage/triggers', controller: Index, role: ['Admin'] }, 'NavBarLevel2' )
|
|
||||||
|
|
27
app/assets/javascripts/app/models/job.js.coffee
Normal file
27
app/assets/javascripts/app/models/job.js.coffee
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
class App.Job extends App.Model
|
||||||
|
@configure 'Job', 'name', 'timeplan', 'condition', 'execute', 'note', 'active'
|
||||||
|
@extend Spine.Model.Ajax
|
||||||
|
@url: @apiPath + '/jobs'
|
||||||
|
@configure_attributes = [
|
||||||
|
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false },
|
||||||
|
{ name: 'timeplan', display: 'The times where the job should run.', tag: 'timeplan', null: true },
|
||||||
|
{ name: 'condition', display: 'Conditions for matching objects.', tag: 'ticket_attribute_selection', null: true },
|
||||||
|
{ name: 'execute', display: 'Execute changes on objects.', tag: 'ticket_attribute_set', null: true },
|
||||||
|
{ name: 'note', display: 'Note', tag: 'textarea', note: 'Notes are visible to agents only, never to customers.', limit: 250, null: true },
|
||||||
|
{ name: 'active', display: 'Active', tag: 'boolean', note: 'boolean', 'default': true, null: false },
|
||||||
|
{ name: 'matching', display: 'Matching', readonly: 1 },
|
||||||
|
{ name: 'processed', display: 'Processed', readonly: 1 },
|
||||||
|
{ name: 'last_run_at', display: 'Last run', type: 'time', readonly: 1 },
|
||||||
|
{ name: 'running', display: 'Running', tag: 'boolean', readonly: 1 },
|
||||||
|
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 },
|
||||||
|
{ name: 'created_at', display: 'Created', type: 'time', readonly: 1 },
|
||||||
|
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 },
|
||||||
|
{ name: 'updated_at', display: 'Updated', type: 'time', readonly: 1 },
|
||||||
|
]
|
||||||
|
@configure_delete = true
|
||||||
|
@configure_overview = [
|
||||||
|
'name',
|
||||||
|
'last_run_at',
|
||||||
|
'matching',
|
||||||
|
'processed',
|
||||||
|
]
|
|
@ -3,4 +3,3 @@
|
||||||
<hr>
|
<hr>
|
||||||
<div class="ticket_attribute_list"></div>
|
<div class="ticket_attribute_list"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
11
app/assets/javascripts/app/views/generic/timeplan.jst.eco
Normal file
11
app/assets/javascripts/app/views/generic/timeplan.jst.eco
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<div>
|
||||||
|
<label><%- @T('Days') %>
|
||||||
|
<div class="days"></div>
|
||||||
|
</label>
|
||||||
|
<label><%- @T('Hours') %>
|
||||||
|
<div class="hours"></div>
|
||||||
|
</label>
|
||||||
|
<label><%- @T('Minutes') %>
|
||||||
|
<div class="minutes"></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
|
@ -1,91 +0,0 @@
|
||||||
<div class="page-header-title">
|
|
||||||
<h1>Scheduler <small>Management</small></h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul class="nav nav-tabs nav-stacked">
|
|
||||||
<li><a href="#">Jobs</a></li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div class="table-overview">
|
|
||||||
<div class="tabbable">
|
|
||||||
<ul class="nav nav-tabs">
|
|
||||||
<li class="active"><a href="#channel-inbound" data-toggle="tab">Times</a></li>
|
|
||||||
<li><a href="#channel-outbound" data-toggle="tab">Properties</a></li>
|
|
||||||
<li><a href="#channel-filter" data-toggle="tab">Message</a></li>
|
|
||||||
</ul>
|
|
||||||
<div class="tab-content">
|
|
||||||
<div class="tab-pane active" id="channel-inbound">
|
|
||||||
<table class="table table-striped">
|
|
||||||
<tr>
|
|
||||||
<th>Host</th>
|
|
||||||
<th>User</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Active</th>
|
|
||||||
<th>Delete</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>lalal.example.com</td>
|
|
||||||
<td>wpt234rwr</td>
|
|
||||||
<td>IMAP</td>
|
|
||||||
<td>true</td>
|
|
||||||
<td>x</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>l31alal.example.com</td>
|
|
||||||
<td>wpt23dd4rwr</td>
|
|
||||||
<td>POP3</td>
|
|
||||||
<td>true</td>
|
|
||||||
<td>x</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="tab-pane" id="channel-outbound">
|
|
||||||
<table class="table table-striped">
|
|
||||||
<tr>
|
|
||||||
<th>Host</th>
|
|
||||||
<th>User</th>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Active</th>
|
|
||||||
<th>Delete</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>lalal.example.com</td>
|
|
||||||
<td>wpt234rwr</td>
|
|
||||||
<td>SMTP</td>
|
|
||||||
<td>true</td>
|
|
||||||
<td>x</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>l31alal.example.com</td>
|
|
||||||
<td>wpt23dd4rwr</td>
|
|
||||||
<td>Sendmail</td>
|
|
||||||
<td>true</td>
|
|
||||||
<td>x</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="tab-pane" id="channel-filter">
|
|
||||||
<table class="table table-striped">
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Active</th>
|
|
||||||
<th>Last Run</th>
|
|
||||||
<th>Delete</th>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>lalal.example.com</td>
|
|
||||||
<td>true</td>
|
|
||||||
<td>true</td>
|
|
||||||
<td>x</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>wpt23dd4rwr</td>
|
|
||||||
<td>true</td>
|
|
||||||
<td>true</td>
|
|
||||||
<td>x</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -23,6 +23,13 @@ class TestsController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# GET /tests/from_extended
|
||||||
|
def form
|
||||||
|
respond_to do |format|
|
||||||
|
format.html # index.html.erb
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# GET /tests/table
|
# GET /tests/table
|
||||||
def table
|
def table
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
|
|
88
app/models/job.rb
Normal file
88
app/models/job.rb
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/
|
||||||
|
|
||||||
|
class Job < ApplicationModel
|
||||||
|
store :timeplan
|
||||||
|
store :condition
|
||||||
|
store :execute
|
||||||
|
validates :name, :presence => true
|
||||||
|
|
||||||
|
before_create :updated_matching
|
||||||
|
before_update :updated_matching
|
||||||
|
|
||||||
|
after_create :notify_clients_after_create
|
||||||
|
after_update :notify_clients_after_update
|
||||||
|
after_destroy :notify_clients_after_destroy
|
||||||
|
|
||||||
|
def self.run
|
||||||
|
time = Time.new
|
||||||
|
day_map = {
|
||||||
|
0 => 'sun',
|
||||||
|
1 => 'mon',
|
||||||
|
2 => 'tue',
|
||||||
|
3 => 'wed',
|
||||||
|
4 => 'thu',
|
||||||
|
5 => 'fri',
|
||||||
|
6 => 'sat',
|
||||||
|
}
|
||||||
|
jobs = Job.where( :active => true )
|
||||||
|
jobs.each do |job|
|
||||||
|
|
||||||
|
# only execute jobs, older then 2 min, to give admin posibility to change
|
||||||
|
next if job.updated_at > Time.now - 2.minutes
|
||||||
|
|
||||||
|
# check if jobs need to be executed
|
||||||
|
# ignore if job was running within last 10 min.
|
||||||
|
next if job.last_run_at > Time.now - 10.minutes
|
||||||
|
|
||||||
|
# check day
|
||||||
|
next if !job.timeplan['days'].include?( day_map[time.wday] )
|
||||||
|
|
||||||
|
# check hour
|
||||||
|
next if !job.timeplan['hours'].include?( time.hour.to_s )
|
||||||
|
|
||||||
|
# check min
|
||||||
|
next if !job.timeplan['minutes'].include?( match_minutes(time.min.to_s) )
|
||||||
|
|
||||||
|
# find tickets to change
|
||||||
|
tickets = Ticket.where( job.condition.permit! ).
|
||||||
|
order( '`tickets`.`created_at` DESC' ).
|
||||||
|
limit( 1_000 )
|
||||||
|
job.processed = tickets.count
|
||||||
|
tickets.each do |ticket|
|
||||||
|
#puts "CHANGE #{job.execute.inspect}"
|
||||||
|
changed = false
|
||||||
|
job.execute.each do |key, value|
|
||||||
|
changed = true
|
||||||
|
attribute = key.split('.', 2).last
|
||||||
|
#puts "-- #{Ticket.columns_hash[ attribute ].type.to_s}"
|
||||||
|
#value = 4
|
||||||
|
#if Ticket.columns_hash[ attribute ].type == :integer
|
||||||
|
# puts "to i #{attribute}/#{value.inspect}/#{value.to_i.inspect}"
|
||||||
|
# #value = value.to_i
|
||||||
|
#end
|
||||||
|
ticket[attribute] = value
|
||||||
|
#puts "set #{attribute} = #{value.inspect}"
|
||||||
|
end
|
||||||
|
next if !changed
|
||||||
|
ticket.updated_by_id = 1
|
||||||
|
ticket.save
|
||||||
|
end
|
||||||
|
|
||||||
|
job.last_run_at = Time.now
|
||||||
|
job.save
|
||||||
|
end
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def updated_matching
|
||||||
|
count = Ticket.where( self.condition.permit! ).count
|
||||||
|
self.matching = count
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.match_minutes(minutes)
|
||||||
|
minutes.gsub!(/(\d)\d/, "\\1")
|
||||||
|
minutes.to_s + '0'
|
||||||
|
end
|
||||||
|
end
|
11
config/routes/job.rb
Normal file
11
config/routes/job.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
Zammad::Application.routes.draw do
|
||||||
|
api_path = Rails.configuration.api_path
|
||||||
|
|
||||||
|
# jobs
|
||||||
|
match api_path + '/jobs', :to => 'jobs#index', :via => :get
|
||||||
|
match api_path + '/jobs/:id', :to => 'jobs#show', :via => :get
|
||||||
|
match api_path + '/jobs', :to => 'jobs#create', :via => :post
|
||||||
|
match api_path + '/jobs/:id', :to => 'jobs#update', :via => :put
|
||||||
|
match api_path + '/jobs/:id', :to => 'jobs#destroy', :via => :delete
|
||||||
|
|
||||||
|
end
|
|
@ -4,6 +4,7 @@ Zammad::Application.routes.draw do
|
||||||
match '/tests-ui', :to => 'tests#ui', :via => :get
|
match '/tests-ui', :to => 'tests#ui', :via => :get
|
||||||
match '/tests-model', :to => 'tests#model', :via => :get
|
match '/tests-model', :to => 'tests#model', :via => :get
|
||||||
match '/tests-form', :to => 'tests#form', :via => :get
|
match '/tests-form', :to => 'tests#form', :via => :get
|
||||||
|
match '/tests-form-extended', :to => 'tests#form_extended', :via => :get
|
||||||
match '/tests-table', :to => 'tests#table', :via => :get
|
match '/tests-table', :to => 'tests#table', :via => :get
|
||||||
match '/tests/wait/:sec', :to => 'tests#wait', :via => :get
|
match '/tests/wait/:sec', :to => 'tests#wait', :via => :get
|
||||||
|
|
||||||
|
|
25
db/migrate/20141221000001_create_job.rb
Normal file
25
db/migrate/20141221000001_create_job.rb
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
class CreateJob < ActiveRecord::Migration
|
||||||
|
def up
|
||||||
|
create_table :jobs do |t|
|
||||||
|
t.column :name, :string, :limit => 250, :null => false
|
||||||
|
t.column :timeplan, :string, :limit => 500, :null => false
|
||||||
|
t.column :condition, :string, :limit => 2500, :null => false
|
||||||
|
t.column :execute, :string, :limit => 2500, :null => false
|
||||||
|
t.column :last_run_at, :timestamp, :null => true
|
||||||
|
t.column :running, :boolean, :null => false, :default => false
|
||||||
|
t.column :processed, :integer, :null => false, :default => 0
|
||||||
|
t.column :matching, :integer, :null => false
|
||||||
|
t.column :pid, :string, :limit => 250, :null => true
|
||||||
|
t.column :note, :string, :limit => 250, :null => true
|
||||||
|
t.column :active, :boolean, :null => false, :default => false
|
||||||
|
t.column :updated_by_id, :integer, :null => false
|
||||||
|
t.column :created_by_id, :integer, :null => false
|
||||||
|
t.timestamps
|
||||||
|
end
|
||||||
|
add_index :jobs, [:name], :unique => true
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
drop_table :jobs
|
||||||
|
end
|
||||||
|
end
|
87
public/assets/tests/form-extended.js
Normal file
87
public/assets/tests/form-extended.js
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
|
||||||
|
// form
|
||||||
|
test( "form simple checks", function() {
|
||||||
|
|
||||||
|
App.TicketPriority.refresh( [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: '1 low',
|
||||||
|
note: 'some note 1',
|
||||||
|
active: true,
|
||||||
|
created_at: '2014-06-10T11:17:34.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: '2 normal',
|
||||||
|
note: 'some note 2',
|
||||||
|
active: false,
|
||||||
|
created_at: '2014-06-10T10:17:34.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: '3 high',
|
||||||
|
note: 'some note 3',
|
||||||
|
active: true,
|
||||||
|
created_at: '2014-06-10T10:17:44.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: '4 very high',
|
||||||
|
note: 'some note 4',
|
||||||
|
active: true,
|
||||||
|
created_at: '2014-06-10T10:17:54.000Z',
|
||||||
|
},
|
||||||
|
] )
|
||||||
|
|
||||||
|
// timeplan
|
||||||
|
$('#forms').append('<hr><h1>form time check</h1><form id="form1"></form>')
|
||||||
|
|
||||||
|
var el = $('#form1')
|
||||||
|
var defaults = {
|
||||||
|
times: {
|
||||||
|
days: ['mon', 'wed'],
|
||||||
|
hours: [2],
|
||||||
|
},
|
||||||
|
conditions: {
|
||||||
|
'tickets.title': 'some title',
|
||||||
|
'tickets.priority_id': [1,2,3],
|
||||||
|
},
|
||||||
|
executions: {
|
||||||
|
'tickets.title': 'some title new',
|
||||||
|
'tickets.priority_id': 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
new App.ControllerForm({
|
||||||
|
el: el,
|
||||||
|
model: {
|
||||||
|
configure_attributes: [
|
||||||
|
{ name: 'times', display: 'Times', tag: 'timeplan', null: true, default: defaults['times'] },
|
||||||
|
{ name: 'conditions', display: 'Conditions', tag: 'ticket_attribute_selection', null: true, default: defaults['conditions'] },
|
||||||
|
{ name: 'executions', display: 'Executions', tag: 'ticket_attribute_set', null: true, default: defaults['executions'] },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
autofocus: true
|
||||||
|
});
|
||||||
|
deepEqual( el.find('[name="times::days"]').val(), ['mon', 'wed'], 'check times::days value')
|
||||||
|
equal( el.find('[name="times::hours"]').val(), 2, 'check times::hours value')
|
||||||
|
equal( el.find('[name="times::minutes"]').val(), null, 'check times::minutes value')
|
||||||
|
|
||||||
|
var params = App.ControllerForm.params( el )
|
||||||
|
var test_params = {
|
||||||
|
times: {
|
||||||
|
days: ['mon', 'wed'],
|
||||||
|
hours: '2',
|
||||||
|
},
|
||||||
|
conditions: {
|
||||||
|
'tickets.title': 'some title',
|
||||||
|
'tickets.priority_id': ['1','3'],
|
||||||
|
},
|
||||||
|
executions: {
|
||||||
|
'tickets.title': 'some title new',
|
||||||
|
'tickets.priority_id': '3',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
deepEqual( params, test_params, 'form param check' );
|
||||||
|
|
||||||
|
|
||||||
|
});
|
|
@ -90,6 +90,28 @@ class AAbUnitTest < TestCase
|
||||||
]
|
]
|
||||||
browser_single_test(tests)
|
browser_single_test(tests)
|
||||||
end
|
end
|
||||||
|
def test_form_extended
|
||||||
|
tests = [
|
||||||
|
{
|
||||||
|
:name => 'start',
|
||||||
|
:instance => browser_instance,
|
||||||
|
:url => browser_url + '/tests-form-extended',
|
||||||
|
:action => [
|
||||||
|
{
|
||||||
|
:execute => 'wait',
|
||||||
|
:value => 8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
:execute => 'match',
|
||||||
|
:css => '.result .failed',
|
||||||
|
:value => '0',
|
||||||
|
:match_result => true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
browser_single_test(tests)
|
||||||
|
end
|
||||||
def test_table
|
def test_table
|
||||||
tests = [
|
tests = [
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue