Fixes #3917 - Multiselect field support.
This commit is contained in:
parent
db0eb45fe1
commit
93932055dd
36 changed files with 1739 additions and 204 deletions
|
@ -531,7 +531,10 @@ class App.ControllerForm extends App.Controller
|
||||||
else
|
else
|
||||||
param[item.name].push value
|
param[item.name].push value
|
||||||
else
|
else
|
||||||
param[item.name] = value
|
if item.multiselect && typeof value is 'string'
|
||||||
|
param[item.name] = new Array(value)
|
||||||
|
else
|
||||||
|
param[item.name] = value
|
||||||
|
|
||||||
# verify if we have not checked checkboxes
|
# verify if we have not checked checkboxes
|
||||||
uncheckParam = {}
|
uncheckParam = {}
|
||||||
|
|
|
@ -29,6 +29,7 @@ class App.UiElement.ApplicationSelector
|
||||||
'integer$': [__('is'), __('is not')]
|
'integer$': [__('is'), __('is not')]
|
||||||
'^radio$': [__('is'), __('is not')]
|
'^radio$': [__('is'), __('is not')]
|
||||||
'^select$': [__('is'), __('is not')]
|
'^select$': [__('is'), __('is not')]
|
||||||
|
'^multiselect$': [__('contains all'), __('contains one'), __('contains all not'), __('contains one not')]
|
||||||
'^tree_select$': [__('is'), __('is not')]
|
'^tree_select$': [__('is'), __('is not')]
|
||||||
'^input$': [__('contains'), __('contains not')]
|
'^input$': [__('contains'), __('contains not')]
|
||||||
'^richtext$': [__('contains'), __('contains not')]
|
'^richtext$': [__('contains'), __('contains not')]
|
||||||
|
@ -44,6 +45,7 @@ class App.UiElement.ApplicationSelector
|
||||||
'integer$': [__('is'), __('is not'), __('has changed')]
|
'integer$': [__('is'), __('is not'), __('has changed')]
|
||||||
'^radio$': [__('is'), __('is not'), __('has changed')]
|
'^radio$': [__('is'), __('is not'), __('has changed')]
|
||||||
'^select$': [__('is'), __('is not'), __('has changed')]
|
'^select$': [__('is'), __('is not'), __('has changed')]
|
||||||
|
'^multiselect$': [__('contains all'), __('contains one'), __('contains all not'), __('contains one not')]
|
||||||
'^tree_select$': [__('is'), __('is not'), __('has changed')]
|
'^tree_select$': [__('is'), __('is not'), __('has changed')]
|
||||||
'^input$': [__('contains'), __('contains not'), __('has changed')]
|
'^input$': [__('contains'), __('contains not'), __('has changed')]
|
||||||
'^richtext$': [__('contains'), __('contains not'), __('has changed')]
|
'^richtext$': [__('contains'), __('contains not'), __('has changed')]
|
||||||
|
|
|
@ -72,6 +72,27 @@ class App.UiElement.ApplicationUiElement
|
||||||
}
|
}
|
||||||
attribute.sortBy = null
|
attribute.sortBy = null
|
||||||
|
|
||||||
|
@getConfigCustomSortOptionList: (attribute) ->
|
||||||
|
if attribute.customsort && attribute.customsort is 'on'
|
||||||
|
if !_.isEmpty(attribute.options)
|
||||||
|
selection = attribute.options
|
||||||
|
attribute.options = []
|
||||||
|
if _.isArray(selection)
|
||||||
|
attribute.options = @getConfigOptionListArray(attribute, selection)
|
||||||
|
else
|
||||||
|
keys = _.keys(selection)
|
||||||
|
for key in keys
|
||||||
|
name_new = selection[key]
|
||||||
|
if attribute.translate
|
||||||
|
name_new = App.i18n.translatePlain(name_new)
|
||||||
|
attribute.options.push {
|
||||||
|
name: name_new
|
||||||
|
value: key
|
||||||
|
}
|
||||||
|
attribute.sortBy = null
|
||||||
|
else
|
||||||
|
@getConfigOptionList(attribute)
|
||||||
|
|
||||||
@getRelationOptionList: (attribute, params) ->
|
@getRelationOptionList: (attribute, params) ->
|
||||||
|
|
||||||
# build options list based on relation
|
# build options list based on relation
|
||||||
|
|
|
@ -53,6 +53,7 @@ class App.UiElement.core_workflow_condition extends App.UiElement.ApplicationSel
|
||||||
'boolean$': [__('is'), __('is not'), __('is set'), __('not set'), _('has changed'), __('changed to')]
|
'boolean$': [__('is'), __('is not'), __('is set'), __('not set'), _('has changed'), __('changed to')]
|
||||||
'integer$': [__('is'), __('is not'), __('is set'), __('not set'), _('has changed'), __('changed to')]
|
'integer$': [__('is'), __('is not'), __('is set'), __('not set'), _('has changed'), __('changed to')]
|
||||||
'^select$': [__('is'), __('is not'), __('is set'), __('not set'), _('has changed'), __('changed to')]
|
'^select$': [__('is'), __('is not'), __('is set'), __('not set'), _('has changed'), __('changed to')]
|
||||||
|
'^multiselect$': [__('contains'), __('contains not'), __('contains all'), __('contains all not'), __('is set'), __('not set'), __('has changed'), __('changed to')]
|
||||||
'^tree_select$': [__('is'), __('is not'), __('is set'), __('not set'), _('has changed'), __('changed to')]
|
'^tree_select$': [__('is'), __('is not'), __('is set'), __('not set'), _('has changed'), __('changed to')]
|
||||||
'^(input|textarea|richtext)$': [__('is'), __('is not'), __('is set'), __('not set'), _('has changed'), __('changed to'), __('regex match'), __('regex mismatch')]
|
'^(input|textarea|richtext)$': [__('is'), __('is not'), __('is set'), __('not set'), _('has changed'), __('changed to'), __('regex match'), __('regex mismatch')]
|
||||||
|
|
||||||
|
@ -147,7 +148,7 @@ class App.UiElement.core_workflow_condition extends App.UiElement.ApplicationSel
|
||||||
}
|
}
|
||||||
|
|
||||||
for row in App[groupMeta.model].configure_attributes
|
for row in App[groupMeta.model].configure_attributes
|
||||||
continue if !_.contains(['input', 'textarea', 'richtext', 'select', 'integer', 'boolean', 'active', 'tree_select', 'autocompletion_ajax'], row.tag)
|
continue if !_.contains(['input', 'textarea', 'richtext', 'multiselect', 'select', 'integer', 'boolean', 'active', 'tree_select', 'autocompletion_ajax'], row.tag)
|
||||||
continue if groupKey is 'ticket' && _.contains(['number', 'title'], row.name)
|
continue if groupKey is 'ticket' && _.contains(['number', 'title'], row.name)
|
||||||
|
|
||||||
# ignore passwords and relations
|
# ignore passwords and relations
|
||||||
|
@ -155,7 +156,7 @@ class App.UiElement.core_workflow_condition extends App.UiElement.ApplicationSel
|
||||||
config = _.clone(row)
|
config = _.clone(row)
|
||||||
if config.tag is 'textarea'
|
if config.tag is 'textarea'
|
||||||
config.expanding = false
|
config.expanding = false
|
||||||
if config.tag is 'select'
|
if /^((multi)?select)$/.test(config.tag)
|
||||||
config.multiple = true
|
config.multiple = true
|
||||||
config.default = undefined
|
config.default = undefined
|
||||||
if config.type is 'email' || config.type is 'tel'
|
if config.type is 'email' || config.type is 'tel'
|
||||||
|
|
|
@ -40,7 +40,7 @@ class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelec
|
||||||
'boolean$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'add_option', 'remove_option', 'set_fixed_to']
|
'boolean$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'add_option', 'remove_option', 'set_fixed_to']
|
||||||
'integer$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly']
|
'integer$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly']
|
||||||
'^date': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly']
|
'^date': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly']
|
||||||
'^select$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'add_option', 'remove_option', 'set_fixed_to', 'select', 'auto_select']
|
'^(multi)?select$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'add_option', 'remove_option', 'set_fixed_to', 'select', 'auto_select']
|
||||||
'^tree_select$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'add_option', 'remove_option', 'set_fixed_to', 'select', 'auto_select']
|
'^tree_select$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'add_option', 'remove_option', 'set_fixed_to', 'select', 'auto_select']
|
||||||
'^(input|textarea)$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'fill_in', 'fill_in_empty']
|
'^(input|textarea)$': ['show', 'hide', 'remove', 'set_mandatory', 'set_optional', 'set_readonly', 'unset_readonly', 'fill_in', 'fill_in_empty']
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelec
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for row in App[groupMeta.model].configure_attributes
|
for row in App[groupMeta.model].configure_attributes
|
||||||
continue if !_.contains(['input', 'textarea', 'select', 'integer', 'boolean', 'tree_select', 'date', 'datetime'], row.tag)
|
continue if !_.contains(['input', 'textarea', 'select', 'multiselect', 'integer', 'boolean', 'tree_select', 'date', 'datetime'], row.tag)
|
||||||
continue if _.contains(['created_at', 'updated_at'], row.name)
|
continue if _.contains(['created_at', 'updated_at'], row.name)
|
||||||
continue if groupKey is 'ticket' && _.contains(['number', 'organization_id', 'title', 'escalation_at', 'first_response_escalation_at', 'update_escalation_at', 'close_escalation_at', 'last_contact_at', 'last_contact_agent_at', 'last_contact_customer_at', 'first_response_at', 'close_at'], row.name)
|
continue if groupKey is 'ticket' && _.contains(['number', 'organization_id', 'title', 'escalation_at', 'first_response_escalation_at', 'update_escalation_at', 'close_escalation_at', 'last_contact_at', 'last_contact_agent_at', 'last_contact_customer_at', 'first_response_at', 'close_at'], row.name)
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelec
|
||||||
config = _.clone(row)
|
config = _.clone(row)
|
||||||
if config.tag is 'boolean'
|
if config.tag is 'boolean'
|
||||||
config.tag = 'select'
|
config.tag = 'select'
|
||||||
if config.tag is 'select'
|
if /^((multi)?select)$/.test(config.tag)
|
||||||
config.multiple = true
|
config.multiple = true
|
||||||
config.default = undefined
|
config.default = undefined
|
||||||
if config.type is 'email' || config.type is 'tel'
|
if config.type is 'email' || config.type is 'tel'
|
||||||
|
@ -121,14 +121,14 @@ class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelec
|
||||||
currentOperator = elementRow.find('.js-operator option:selected').attr('value')
|
currentOperator = elementRow.find('.js-operator option:selected').attr('value')
|
||||||
name = @buildValueName(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
name = @buildValueName(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||||
|
|
||||||
if !_.contains(['add_option', 'remove_option', 'set_fixed_to', 'select', 'execute', 'fill_in', 'fill_in_empty'], currentOperator)
|
if !_.contains(['add_option', 'remove_option', 'set_fixed_to', 'select', 'multiselect', 'execute', 'fill_in', 'fill_in_empty'], currentOperator)
|
||||||
elementRow.find('.js-value').addClass('hide').html('<input type="hidden" name="' + name + '" value="true" />')
|
elementRow.find('.js-value').addClass('hide').html('<input type="hidden" name="' + name + '" value="true" />')
|
||||||
return
|
return
|
||||||
|
|
||||||
super(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
super(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
|
||||||
|
|
||||||
@buildValueConfigMultiple: (config, meta) ->
|
@buildValueConfigMultiple: (config, meta) ->
|
||||||
if _.contains(['add_option', 'remove_option', 'set_fixed_to', 'select'], meta.operator)
|
if _.contains(['add_option', 'remove_option', 'set_fixed_to', 'select', 'multiselect'], meta.operator)
|
||||||
config.multiple = true
|
config.multiple = true
|
||||||
config.nulloption = true
|
config.nulloption = true
|
||||||
else
|
else
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
# coffeelint: disable=camel_case_classes
|
||||||
|
class App.UiElement.multiselect extends App.UiElement.ApplicationUiElement
|
||||||
|
@render: (attribute, params, form = {}) ->
|
||||||
|
|
||||||
|
# set multiple option
|
||||||
|
attribute.multiple = 'multiple'
|
||||||
|
|
||||||
|
if attribute.class
|
||||||
|
attribute.class = "#{attribute.class} multiselect"
|
||||||
|
else
|
||||||
|
attribute.class = 'multiselect'
|
||||||
|
|
||||||
|
if form.rejectNonExistentValues
|
||||||
|
attribute.rejectNonExistentValues = true
|
||||||
|
|
||||||
|
# add deleted historical options if required
|
||||||
|
@addDeletedOptions(attribute, params)
|
||||||
|
|
||||||
|
# build options list based on config
|
||||||
|
@getConfigCustomSortOptionList(attribute)
|
||||||
|
|
||||||
|
# build options list based on relation
|
||||||
|
@getRelationOptionList(attribute, params)
|
||||||
|
|
||||||
|
# sort attribute.options
|
||||||
|
@sortOptions(attribute, params)
|
||||||
|
|
||||||
|
# find selected/checked item of list
|
||||||
|
@selectedOptions(attribute, params)
|
||||||
|
|
||||||
|
# disable item of list
|
||||||
|
@disabledOptions(attribute, params)
|
||||||
|
|
||||||
|
# filter attributes
|
||||||
|
@filterOption(attribute, params)
|
||||||
|
|
||||||
|
# return item
|
||||||
|
$( App.view('generic/select')(attribute: attribute) )
|
||||||
|
|
||||||
|
# 1. If attribute.value is not among the current options, then search within historical options
|
||||||
|
# 2. If attribute.value is not among current and historical options, then add the value itself as an option
|
||||||
|
@addDeletedOptions: (attribute) ->
|
||||||
|
return if !_.isEmpty(attribute.relation) # do not apply for attributes with relation, relations will fill options automatically
|
||||||
|
return if attribute.rejectNonExistentValues
|
||||||
|
value = attribute.value
|
||||||
|
return if !value
|
||||||
|
return if _.isArray(value)
|
||||||
|
return if !attribute.options
|
||||||
|
return if !_.isObject(attribute.options)
|
||||||
|
return if value of attribute.options
|
||||||
|
return if value in (temp for own prop, temp of attribute.options)
|
||||||
|
|
||||||
|
if _.isArray(attribute.options)
|
||||||
|
# Array of Strings (value)
|
||||||
|
return if value of attribute.options
|
||||||
|
|
||||||
|
# Array of Objects (for ordering purposes)
|
||||||
|
return if attribute.options.filter((elem) -> elem.value == value) isnt null
|
||||||
|
else
|
||||||
|
# regular Object
|
||||||
|
return if value in (temp for own prop, temp of attribute.options)
|
||||||
|
|
||||||
|
if attribute.historical_options && value of attribute.historical_options
|
||||||
|
attribute.options[value] = attribute.historical_options[value]
|
||||||
|
else
|
||||||
|
attribute.options[value] = value
|
||||||
|
|
||||||
|
@_selectedOptionsIsSelected: (value, record) ->
|
||||||
|
if _.isArray(value)
|
||||||
|
for valueItem in value
|
||||||
|
if @_selectedOptionsIsSelectedItem(valueItem, record)
|
||||||
|
return true
|
||||||
|
false
|
|
@ -7,14 +7,8 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
|
||||||
if params.data_option_new && !_.isEmpty(params.data_option_new)
|
if params.data_option_new && !_.isEmpty(params.data_option_new)
|
||||||
params.data_option = params.data_option_new
|
params.data_option = params.data_option_new
|
||||||
|
|
||||||
if attribute.value == 'select' && params.data_option? && params.data_option.options?
|
if /^((multi)?select)$/.test(attribute.value) && params.data_option? && params.data_option.options?
|
||||||
sorted = _.map(
|
params.data_option.mapped = @mapDataOptions(params.data_option)
|
||||||
params.data_option.options, (value, key) ->
|
|
||||||
key = '' if !key || !key.toString
|
|
||||||
value = '' if !value || !value.toString
|
|
||||||
[key.toString(), value.toString()]
|
|
||||||
)
|
|
||||||
params.data_option.sorted = sorted.sort( (a, b) -> a[1].localeCompare(b[1]) )
|
|
||||||
|
|
||||||
item = $(App.view('object_manager/attribute')(attribute: attribute))
|
item = $(App.view('object_manager/attribute')(attribute: attribute))
|
||||||
|
|
||||||
|
@ -28,6 +22,7 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
|
||||||
localItem = localForm.closest('.js-data')
|
localItem = localForm.closest('.js-data')
|
||||||
localItem.find('.js-dataMap').html(element)
|
localItem.find('.js-dataMap').html(element)
|
||||||
localItem.find('.js-dataScreens').html(@dataScreens(attribute, localParams, params))
|
localItem.find('.js-dataScreens').html(@dataScreens(attribute, localParams, params))
|
||||||
|
@addDragAndDrop(localItem)
|
||||||
|
|
||||||
options =
|
options =
|
||||||
datetime: __('Datetime')
|
datetime: __('Datetime')
|
||||||
|
@ -38,6 +33,7 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
|
||||||
tree_select: __('Tree Select')
|
tree_select: __('Tree Select')
|
||||||
boolean: __('Boolean')
|
boolean: __('Boolean')
|
||||||
integer: __('Integer')
|
integer: __('Integer')
|
||||||
|
multiselect: __('Multiselect')
|
||||||
|
|
||||||
# if attribute already exists, do not allow to change it anymore
|
# if attribute already exists, do not allow to change it anymore
|
||||||
if params.data_type
|
if params.data_type
|
||||||
|
@ -373,6 +369,56 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
|
||||||
)
|
)
|
||||||
item.find('.js-inputLinkTemplate').html(inputLinkTemplate.form)
|
item.find('.js-inputLinkTemplate').html(inputLinkTemplate.form)
|
||||||
|
|
||||||
|
@multiselect: (item, localParams, params) ->
|
||||||
|
item.find('.js-add').on('click', (e) ->
|
||||||
|
addRow = $(e.target).closest('tr')
|
||||||
|
key = addRow.find('.js-key').val()
|
||||||
|
value = addRow.find('.js-value').val()
|
||||||
|
addRow.find('.js-selected[value]').attr('value', key)
|
||||||
|
selected = addRow.find('.js-selected').prop('checked')
|
||||||
|
newRow = item.find('.js-template').clone().removeClass('js-template')
|
||||||
|
newRow.find('.js-key').val(key)
|
||||||
|
newRow.find('.js-value').val(value)
|
||||||
|
newRow.find('.js-value[value]').attr('name', "data_option::options::#{key}")
|
||||||
|
newRow.find('.js-selected').prop('checked', selected)
|
||||||
|
newRow.find('.js-selected').val(key)
|
||||||
|
newRow.find('.js-selected').attr('name', 'data_option::default')
|
||||||
|
item.find('.js-Table tr').last().before(newRow)
|
||||||
|
addRow.find('.js-key').val('')
|
||||||
|
addRow.find('.js-value').val('')
|
||||||
|
addRow.find('.js-selected').prop('checked', false)
|
||||||
|
)
|
||||||
|
item.on('change', '.js-key', (e) ->
|
||||||
|
key = $(e.target).val()
|
||||||
|
valueField = $(e.target).closest('tr').find('.js-value[name]')
|
||||||
|
valueField.attr('name', "data_option::options::#{key}")
|
||||||
|
)
|
||||||
|
item.on('click', '.js-remove', (e) ->
|
||||||
|
$(e.target).closest('tr').remove()
|
||||||
|
)
|
||||||
|
lastSelected = undefined
|
||||||
|
item.on('click', '.js-selected', (e) ->
|
||||||
|
checked = $(e.target).prop('checked')
|
||||||
|
value = $(e.target).attr('value')
|
||||||
|
if checked && lastSelected && lastSelected is value
|
||||||
|
$(e.target).prop('checked', false)
|
||||||
|
lastSelected = false
|
||||||
|
return
|
||||||
|
lastSelected = value
|
||||||
|
)
|
||||||
|
configureAttributes = [
|
||||||
|
# coffeelint: disable=no_interpolation_in_single_quotes
|
||||||
|
{ name: 'data_option::linktemplate', display: 'Link-Template', tag: 'input', type: 'text', null: true, default: '', placeholder: 'https://example.com/?q=#{ticket.attribute_name}' },
|
||||||
|
# coffeelint: enable=no_interpolation_in_single_quotes
|
||||||
|
]
|
||||||
|
inputLinkTemplate = new App.ControllerForm(
|
||||||
|
model:
|
||||||
|
configure_attributes: configureAttributes
|
||||||
|
noFieldset: true
|
||||||
|
params: params
|
||||||
|
)
|
||||||
|
item.find('.js-inputLinkTemplate').html(inputLinkTemplate.form)
|
||||||
|
|
||||||
@buildRow: (element, child, level = 0, parentElement) ->
|
@buildRow: (element, child, level = 0, parentElement) ->
|
||||||
newRow = element.find('.js-template').clone().removeClass('js-template')
|
newRow = element.find('.js-template').clone().removeClass('js-template')
|
||||||
newRow.find('.js-key').attr('level', level)
|
newRow.find('.js-key').attr('level', level)
|
||||||
|
@ -494,3 +540,37 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
|
||||||
item.find('.js-autocompletionDefault').html(autocompletionDefault.form)
|
item.find('.js-autocompletionDefault').html(autocompletionDefault.form)
|
||||||
item.find('.js-autocompletionUrl').html(autocompletionUrl.form)
|
item.find('.js-autocompletionUrl').html(autocompletionUrl.form)
|
||||||
item.find('.js-autocompletionMethod').html(autocompletionMethod.form)
|
item.find('.js-autocompletionMethod').html(autocompletionMethod.form)
|
||||||
|
|
||||||
|
@addDragAndDrop: (item) ->
|
||||||
|
dndOptions =
|
||||||
|
tolerance: 'pointer'
|
||||||
|
distance: 15
|
||||||
|
opacity: 0.6
|
||||||
|
forcePlaceholderSize: true
|
||||||
|
items: 'tr'
|
||||||
|
helper: (e, tr) ->
|
||||||
|
originals = tr.children()
|
||||||
|
helper = tr.clone()
|
||||||
|
helper.children().each (index) ->
|
||||||
|
# Set helper cell sizes to match the original sizes
|
||||||
|
$(@).width( originals.eq(index).outerWidth() )
|
||||||
|
return helper
|
||||||
|
item.find('tbody.table-sortable').sortable(dndOptions)
|
||||||
|
|
||||||
|
@mapDataOptions: ({options, customsort}) ->
|
||||||
|
if _.isArray(options)
|
||||||
|
mappedOptions = options.map(({name, value}) ->
|
||||||
|
value = '' if !value || !value.toString
|
||||||
|
name = '' if !name || !name.toString
|
||||||
|
[value.toString(), name.toString()]
|
||||||
|
)
|
||||||
|
else
|
||||||
|
mappedOptions = _.map(
|
||||||
|
options, (value, key) ->
|
||||||
|
key = '' if !key || !key.toString
|
||||||
|
value = '' if !value || !value.toString
|
||||||
|
[key.toString(), value.toString()]
|
||||||
|
)
|
||||||
|
return mappedOptions if customsort? && customsort is 'on'
|
||||||
|
|
||||||
|
mappedOptions.sort( (a, b) -> a[1].localeCompare(b[1]) )
|
||||||
|
|
|
@ -15,7 +15,7 @@ class App.UiElement.select extends App.UiElement.ApplicationUiElement
|
||||||
@addDeletedOptions(attribute, params)
|
@addDeletedOptions(attribute, params)
|
||||||
|
|
||||||
# build options list based on config
|
# build options list based on config
|
||||||
@getConfigOptionList(attribute, params)
|
@getConfigCustomSortOptionList(attribute)
|
||||||
|
|
||||||
# build options list based on relation
|
# build options list based on relation
|
||||||
@getRelationOptionList(attribute, params)
|
@getRelationOptionList(attribute, params)
|
||||||
|
|
|
@ -39,9 +39,16 @@ treeParams = (e, params) ->
|
||||||
params.data_option.options = tree
|
params.data_option.options = tree
|
||||||
params
|
params
|
||||||
|
|
||||||
|
multiselectParams = (params) ->
|
||||||
|
return params if !params.data_type || params.data_type isnt 'multiselect'
|
||||||
|
|
||||||
|
if typeof params.data_option.default is 'string'
|
||||||
|
params.data_option.default = new Array(params.data_option.default)
|
||||||
|
params
|
||||||
|
|
||||||
setSelectDefaults = (el) ->
|
setSelectDefaults = (el) ->
|
||||||
data_type = el.find('select[name=data_type]').val()
|
data_type = el.find('select[name=data_type]').val()
|
||||||
return if data_type isnt 'select' && data_type isnt 'boolean'
|
return if !/^((multi)?select)$/.test(data_type) && data_type isnt 'boolean'
|
||||||
|
|
||||||
el.find('.js-value, .js-valueTrue, .js-valueFalse').each(->
|
el.find('.js-value, .js-valueTrue, .js-valueFalse').each(->
|
||||||
element = $(@)
|
element = $(@)
|
||||||
|
@ -54,6 +61,19 @@ setSelectDefaults = (el) ->
|
||||||
element.val(key_value)
|
element.val(key_value)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
customsortDataOptions = ({target}, params) ->
|
||||||
|
return params if !params.data_option || params.data_option.customsort isnt 'on'
|
||||||
|
|
||||||
|
options = []
|
||||||
|
$(target).closest('.modal').find('table.js-Table tr.input-data-row').each( ->
|
||||||
|
$element = $(@)
|
||||||
|
name = $element.find('input.js-value').val().trim()
|
||||||
|
value = $element.find('input.js-key').val().trim()
|
||||||
|
options.push({name, value})
|
||||||
|
)
|
||||||
|
params.data_option.options = options
|
||||||
|
params
|
||||||
|
|
||||||
class ObjectManager extends App.ControllerTabs
|
class ObjectManager extends App.ControllerTabs
|
||||||
requiredPermission: 'admin.object'
|
requiredPermission: 'admin.object'
|
||||||
constructor: ->
|
constructor: ->
|
||||||
|
@ -198,6 +218,8 @@ class New extends App.ControllerGenericNew
|
||||||
|
|
||||||
params = @formParam(e.target)
|
params = @formParam(e.target)
|
||||||
params = treeParams(e, params)
|
params = treeParams(e, params)
|
||||||
|
params = multiselectParams(params)
|
||||||
|
params = customsortDataOptions(e, params)
|
||||||
|
|
||||||
# show attributes for create_middle in two column style
|
# show attributes for create_middle in two column style
|
||||||
if params.screens && params.screens.create_middle
|
if params.screens && params.screens.create_middle
|
||||||
|
@ -261,6 +283,8 @@ class Edit extends App.ControllerGenericEdit
|
||||||
|
|
||||||
params = @formParam(e.target)
|
params = @formParam(e.target)
|
||||||
params = treeParams(e, params)
|
params = treeParams(e, params)
|
||||||
|
params = multiselectParams(params)
|
||||||
|
params = customsortDataOptions(e, params)
|
||||||
|
|
||||||
# show attributes for create_middle in two column style
|
# show attributes for create_middle in two column style
|
||||||
if params.screens && params.screens.create_middle
|
if params.screens && params.screens.create_middle
|
||||||
|
|
|
@ -124,16 +124,22 @@ class App.FormHandlerCoreWorkflow
|
||||||
coreWorkflowRestrictions[classname][item.name] = App.FormHandlerCoreWorkflow.restrictValuesAttributeCache(attribute, values)
|
coreWorkflowRestrictions[classname][item.name] = App.FormHandlerCoreWorkflow.restrictValuesAttributeCache(attribute, values)
|
||||||
|
|
||||||
valueFound = false
|
valueFound = false
|
||||||
for value in values
|
if item.tag is 'multiselect'
|
||||||
|
if _.isArray(paramValue)
|
||||||
|
paramValue = _.intersection(paramValue, values)
|
||||||
|
if paramValue.length > 0
|
||||||
|
valueFound = true
|
||||||
|
else
|
||||||
|
for value in values
|
||||||
|
|
||||||
# false values are valid values e.g. for boolean fields (be careful)
|
# false values are valid values e.g. for boolean fields (be careful)
|
||||||
if value isnt undefined && paramValue isnt undefined && value isnt null && paramValue isnt null
|
continue if value is undefined
|
||||||
if value.toString() == paramValue.toString()
|
continue if value is null
|
||||||
valueFound = true
|
continue if paramValue is undefined
|
||||||
break
|
continue if paramValue is null
|
||||||
if _.isArray(paramValue) && _.contains(paramValue, value.toString())
|
continue if value.toString() != paramValue.toString()
|
||||||
valueFound = true
|
valueFound = true
|
||||||
break
|
break
|
||||||
|
|
||||||
item.filter = values
|
item.filter = values
|
||||||
if valueFound
|
if valueFound
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
<div>
|
||||||
|
<table class="settings-list js-Table" style="width: 100%;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width: 36px" class="table-draggable"></th>
|
||||||
|
<th><%- @T('Key') %>
|
||||||
|
<th><%- @T('Display') %>
|
||||||
|
<th style="width: 30px"><%- @T('Default') %>
|
||||||
|
<th style="width: 30px"><%- @T('Action') %>
|
||||||
|
</thead>
|
||||||
|
<tbody class="table-sortable">
|
||||||
|
<% if @params.data_option && @params.data_option.mapped: %>
|
||||||
|
<% for [key, display] in @params.data_option.mapped: %>
|
||||||
|
<tr class="input-data-row">
|
||||||
|
<td class="table-draggable"><%- @Icon('draggable') %></td>
|
||||||
|
<td class="settings-list-control-cell">
|
||||||
|
<input class="form-control form-control--small js-key" type="text" value="<%= key %>" required/>
|
||||||
|
</td>
|
||||||
|
<td class="settings-list-control-cell">
|
||||||
|
<input class="form-control form-control--small js-value" type="text" value="<%= display %>" name="data_option::options::<%= key %>" required/>
|
||||||
|
</td>
|
||||||
|
<td class="settings-list-row-control">
|
||||||
|
<input class="js-selected" type="checkbox" name="data_option::default" value="<%= key %>" <% if _.include(@params.data_option.default, key): %>checked<% end %>/>
|
||||||
|
</td>
|
||||||
|
<td class="settings-list-row-control">
|
||||||
|
<div class="btn btn--text js-remove">
|
||||||
|
<%- @Icon('trash') %> <%- @T('Remove') %>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<tr class="input-add-row">
|
||||||
|
<td class="settings-list-control-cell">
|
||||||
|
<td class="settings-list-control-cell">
|
||||||
|
<input class="form-control form-control--small js-key" type="text" placeholder="<%- @T('Key') %>"/>
|
||||||
|
<td class="settings-list-control-cell">
|
||||||
|
<input class="form-control form-control--small js-value" type="text" placeholder="<%- @T('Display') %>"/>
|
||||||
|
<td class="settings-list-row-control">
|
||||||
|
<input class="js-selected" type="checkbox"/>
|
||||||
|
<td class="settings-list-row-control">
|
||||||
|
<div class="btn btn--text btn--create js-add">
|
||||||
|
<%- @Icon('plus-small') %> <%- @T('Add') %>
|
||||||
|
</div>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<table class="hidden">
|
||||||
|
<tbody>
|
||||||
|
<tr class="js-template input-data-row">
|
||||||
|
<td class="table-draggable"><%- @Icon('draggable') %></td>
|
||||||
|
<td class="settings-list-control-cell">
|
||||||
|
<input class="form-control form-control--small js-key" type="text" value="" required/>
|
||||||
|
<td class="settings-list-control-cell">
|
||||||
|
<input class="form-control form-control--small js-value" type="text" value="" required/>
|
||||||
|
<td class="settings-list-row-control">
|
||||||
|
<input class="js-selected" type="checkbox" name="data_option::default"/>
|
||||||
|
<td class="settings-list-row-control">
|
||||||
|
<div class="btn btn--text js-remove">
|
||||||
|
<%- @Icon('trash') %> <%- @T('Remove') %>
|
||||||
|
</div>
|
||||||
|
</table>
|
||||||
|
<div class="checkbox checkbox--list">
|
||||||
|
<label class="checkbox-replacement">
|
||||||
|
<input type="checkbox" name="data_option::customsort" <% if (@params.data_option && @params.data_option.customsort): %>checked<% end %>/>
|
||||||
|
<%- @Icon('checkbox', 'icon-unchecked') %>
|
||||||
|
<%- @Icon('checkbox-checked', 'icon-checked') %>
|
||||||
|
<span class="label-text"><%- @T('Use custom option sort') %></span>
|
||||||
|
<span class="help-text"><%- @T('Check this box if you want to customise how options are been sorted. If checkbox is disabled, values are sorted in alphabetical order.') %></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="js-inputLinkTemplate"></div>
|
||||||
|
</div>
|
|
@ -2,15 +2,17 @@
|
||||||
<table class="settings-list js-Table" style="width: 100%;">
|
<table class="settings-list js-Table" style="width: 100%;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th style="width: 36px" class="table-draggable"></th>
|
||||||
<th><%- @T('Key') %>
|
<th><%- @T('Key') %>
|
||||||
<th><%- @T('Display') %>
|
<th><%- @T('Display') %>
|
||||||
<th style="width: 30px"><%- @T('Default') %>
|
<th style="width: 30px"><%- @T('Default') %>
|
||||||
<th style="width: 30px"><%- @T('Action') %>
|
<th style="width: 30px"><%- @T('Action') %>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody class="table-sortable">
|
||||||
<% if @params.data_option && @params.data_option.sorted: %>
|
<% if @params.data_option && @params.data_option.mapped: %>
|
||||||
<% for [key, display] in @params.data_option.sorted: %>
|
<% for [key, display] in @params.data_option.mapped: %>
|
||||||
<tr>
|
<tr class="input-data-row">
|
||||||
|
<td class="table-draggable"><%- @Icon('draggable') %></td>
|
||||||
<td class="settings-list-control-cell">
|
<td class="settings-list-control-cell">
|
||||||
<input class="form-control form-control--small js-key" type="text" value="<%= key %>" required/>
|
<input class="form-control form-control--small js-key" type="text" value="<%= key %>" required/>
|
||||||
<td class="settings-list-control-cell">
|
<td class="settings-list-control-cell">
|
||||||
|
@ -23,7 +25,8 @@
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<tr>
|
<tr class="input-add-row">
|
||||||
|
<td class="settings-list-control-cell">
|
||||||
<td class="settings-list-control-cell">
|
<td class="settings-list-control-cell">
|
||||||
<input class="form-control form-control--small js-key" type="text" placeholder="<%- @T('Key') %>"/>
|
<input class="form-control form-control--small js-key" type="text" placeholder="<%- @T('Key') %>"/>
|
||||||
<td class="settings-list-control-cell">
|
<td class="settings-list-control-cell">
|
||||||
|
@ -38,7 +41,8 @@
|
||||||
</table>
|
</table>
|
||||||
<table class="hidden">
|
<table class="hidden">
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr class="js-template">
|
<tr class="js-template input-data-row">
|
||||||
|
<td class="table-draggable"><%- @Icon('draggable') %></td>
|
||||||
<td class="settings-list-control-cell">
|
<td class="settings-list-control-cell">
|
||||||
<input class="form-control form-control--small js-key" type="text" value="" required/>
|
<input class="form-control form-control--small js-key" type="text" value="" required/>
|
||||||
<td class="settings-list-control-cell">
|
<td class="settings-list-control-cell">
|
||||||
|
@ -50,5 +54,14 @@
|
||||||
<%- @Icon('trash') %> <%- @T('Remove') %>
|
<%- @Icon('trash') %> <%- @T('Remove') %>
|
||||||
</div>
|
</div>
|
||||||
</table>
|
</table>
|
||||||
|
<div class="checkbox checkbox--list">
|
||||||
|
<label class="checkbox-replacement">
|
||||||
|
<input type="checkbox" name="data_option::customsort" <% if (@params.data_option && @params.data_option.customsort): %>checked<% end %>/>
|
||||||
|
<%- @Icon('checkbox', 'icon-unchecked') %>
|
||||||
|
<%- @Icon('checkbox-checked', 'icon-checked') %>
|
||||||
|
<span class="label-text"><%- @T('Use custom option sort') %></span>
|
||||||
|
<span class="help-text"><%- @T('Check this box if you want to customise how options are been sorted. If checkbox is disabled, values are sorted in alphabetical order.') %></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div class="js-inputLinkTemplate"></div>
|
<div class="js-inputLinkTemplate"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -225,6 +225,7 @@ jQuery.fn.removeAttrs = function(regex) {
|
||||||
// changes
|
// changes
|
||||||
// - set type based on data('field-type')
|
// - set type based on data('field-type')
|
||||||
// - also catch [disabled] params
|
// - also catch [disabled] params
|
||||||
|
// - return multiselect type to make sure that the data is always array
|
||||||
jQuery.fn.extend( {
|
jQuery.fn.extend( {
|
||||||
serializeArrayWithType: function() {
|
serializeArrayWithType: function() {
|
||||||
var r20 = /%20/g,
|
var r20 = /%20/g,
|
||||||
|
@ -248,27 +249,29 @@ jQuery.fn.extend( {
|
||||||
( this.checked || !rcheckableType.test( type ) );
|
( this.checked || !rcheckableType.test( type ) );
|
||||||
} )
|
} )
|
||||||
.map( function( i, elem ) {
|
.map( function( i, elem ) {
|
||||||
var $elem = jQuery( this );
|
var $elem = jQuery( this );
|
||||||
var val = $elem.val();
|
var val = $elem.val();
|
||||||
var type = $elem.data('field-type');
|
var type = $elem.data('field-type');
|
||||||
|
var multiple = $elem.prop('multiple');
|
||||||
|
var multiselect = multiple && $elem.hasClass('multiselect');
|
||||||
|
|
||||||
var result;
|
var result;
|
||||||
if ( val == null ) {
|
if ( val == null ) {
|
||||||
// be sure that also null values are transferred
|
// be sure that also null values are transferred
|
||||||
// https://github.com/zammad/zammad/issues/944
|
// https://github.com/zammad/zammad/issues/944
|
||||||
if ($elem.prop('multiple')) {
|
if ($elem.prop('multiple')) {
|
||||||
result = { name: elem.name, value: null, type: type }
|
result = { name: elem.name, value: null, type: type, multiselect: multiselect }
|
||||||
} else {
|
} else {
|
||||||
result = null
|
result = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if ( jQuery.isArray( val ) ) {
|
else if ( jQuery.isArray( val ) ) {
|
||||||
result = jQuery.map( val, function( val ) {
|
result = jQuery.map( val, function( val ) {
|
||||||
return { name: elem.name, value: val.replace( rCRLF, "\r\n" ), type: type };
|
return { name: elem.name, value: val.replace( rCRLF, "\r\n" ), type: type, multiselect: multiselect };
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
result = { name: elem.name, value: val.replace( rCRLF, "\r\n" ), type: type };
|
result = { name: elem.name, value: val.replace( rCRLF, "\r\n" ), type: type, multiselect: multiselect };
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
} ).get();
|
} ).get();
|
||||||
|
|
|
@ -3680,6 +3680,7 @@ ol.tabs li {
|
||||||
|
|
||||||
.table-draggable & {
|
.table-draggable & {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
cursor: move;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -98,7 +98,7 @@ class ObjectManagerAttributesController < ApplicationController
|
||||||
if permitted[:data_option]
|
if permitted[:data_option]
|
||||||
|
|
||||||
if !permitted[:data_option].key?(:default)
|
if !permitted[:data_option].key?(:default)
|
||||||
permitted[:data_option][:default] = if permitted[:data_type].match?(%r{^(input|select|tree_select)$})
|
permitted[:data_option][:default] = if permitted[:data_type].match?(%r{^(input|select|multiselect|tree_select)$})
|
||||||
''
|
''
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -39,7 +39,11 @@ module ChecksCoreWorkflow
|
||||||
end
|
end
|
||||||
|
|
||||||
def restricted_value?(perform_result, key)
|
def restricted_value?(perform_result, key)
|
||||||
perform_result[:restrict_values][key].any? { |value| value.to_s == self[key].to_s }
|
if self[key].is_a?(Array)
|
||||||
|
(self[key].map(&:to_s) - perform_result[:restrict_values][key].map(&:to_s)).blank?
|
||||||
|
else
|
||||||
|
perform_result[:restrict_values][key].any? { |value| value.to_s == self[key].to_s }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_mandatory(perform_result)
|
def check_mandatory(perform_result)
|
||||||
|
|
|
@ -234,8 +234,8 @@ class CoreWorkflow::Attributes
|
||||||
return values if values == ['']
|
return values if values == ['']
|
||||||
|
|
||||||
saved_value = saved_attribute_value(attribute)
|
saved_value = saved_attribute_value(attribute)
|
||||||
if saved_value.present? && values.exclude?(saved_value)
|
if saved_value.present?
|
||||||
values |= Array(saved_value.to_s)
|
values |= Array(saved_value).map(&:to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
if attribute[:nulloption] && values.exclude?('')
|
if attribute[:nulloption] && values.exclude?('')
|
||||||
|
|
|
@ -25,19 +25,19 @@ class CoreWorkflow::Result::Backend
|
||||||
def saved_value
|
def saved_value
|
||||||
|
|
||||||
# make sure we have a saved object
|
# make sure we have a saved object
|
||||||
return if @result_object.attributes.saved_only.blank?
|
return [] if @result_object.attributes.saved_only.blank?
|
||||||
|
|
||||||
# we only want to have the saved value in the restrictions
|
# we only want to have the saved value in the restrictions
|
||||||
# if no changes happend to the form. If the users does changes
|
# if no changes happend to the form. If the users does changes
|
||||||
# to the form then also the saved value should get removed
|
# to the form then also the saved value should get removed
|
||||||
return if @result_object.attributes.selected.changed?
|
return [] if @result_object.attributes.selected.changed?
|
||||||
|
|
||||||
# attribute can be blank e.g. in custom development
|
# attribute can be blank e.g. in custom development
|
||||||
# or if attribute is only available in the frontend but not
|
# or if attribute is only available in the frontend but not
|
||||||
# in the backend
|
# in the backend
|
||||||
return if attribute.blank?
|
return [] if attribute.blank?
|
||||||
|
|
||||||
@result_object.attributes.saved_attribute_value(attribute).to_s
|
Array(@result_object.attributes.saved_attribute_value(attribute)).map(&:to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
def attribute
|
def attribute
|
||||||
|
|
|
@ -10,7 +10,7 @@ class CoreWorkflow::Result::RemoveOption < CoreWorkflow::Result::BaseOption
|
||||||
|
|
||||||
def config_value
|
def config_value
|
||||||
result = Array(@perform_config['remove_option'])
|
result = Array(@perform_config['remove_option'])
|
||||||
result -= Array(saved_value)
|
result -= saved_value
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,7 +13,7 @@ class CoreWorkflow::Result::SetFixedTo < CoreWorkflow::Result::BaseOption
|
||||||
|
|
||||||
def config_value
|
def config_value
|
||||||
result = Array(@perform_config['set_fixed_to'])
|
result = Array(@perform_config['set_fixed_to'])
|
||||||
result |= Array(saved_value)
|
result |= saved_value
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ class ObjectManager::Attribute < ApplicationModel
|
||||||
user_autocompletion
|
user_autocompletion
|
||||||
checkbox
|
checkbox
|
||||||
select
|
select
|
||||||
|
multiselect
|
||||||
tree_select
|
tree_select
|
||||||
datetime
|
datetime
|
||||||
date
|
date
|
||||||
|
@ -42,6 +43,9 @@ class ObjectManager::Attribute < ApplicationModel
|
||||||
|
|
||||||
before_validation :set_base_options
|
before_validation :set_base_options
|
||||||
|
|
||||||
|
before_create :ensure_multiselect
|
||||||
|
before_update :ensure_multiselect
|
||||||
|
|
||||||
scope :active, -> { where(active: true) }
|
scope :active, -> { where(active: true) }
|
||||||
scope :editable, -> { where(editable: true) }
|
scope :editable, -> { where(editable: true) }
|
||||||
scope :for_object, lambda { |name_or_klass|
|
scope :for_object, lambda { |name_or_klass|
|
||||||
|
@ -588,11 +592,17 @@ to send no browser reload event, pass false
|
||||||
|
|
||||||
data_type = nil
|
data_type = nil
|
||||||
case attribute.data_type
|
case attribute.data_type
|
||||||
when %r{^input|select|tree_select|richtext|textarea|checkbox$}
|
when %r{^(input|select|tree_select|richtext|textarea|checkbox)$}
|
||||||
data_type = :string
|
data_type = :string
|
||||||
when %r{^integer|user_autocompletion$}
|
when %r{^(multiselect)$}
|
||||||
|
data_type = if Rails.application.config.db_column_array
|
||||||
|
:string
|
||||||
|
else
|
||||||
|
:json
|
||||||
|
end
|
||||||
|
when %r{^(integer|user_autocompletion)$}
|
||||||
data_type = :integer
|
data_type = :integer
|
||||||
when %r{^boolean|active$}
|
when %r{^(boolean|active)$}
|
||||||
data_type = :boolean
|
data_type = :boolean
|
||||||
when %r{^datetime$}
|
when %r{^datetime$}
|
||||||
data_type = :datetime
|
data_type = :datetime
|
||||||
|
@ -603,7 +613,7 @@ to send no browser reload event, pass false
|
||||||
# change field
|
# change field
|
||||||
if model.column_names.include?(attribute.name)
|
if model.column_names.include?(attribute.name)
|
||||||
case attribute.data_type
|
case attribute.data_type
|
||||||
when %r{^input|select|tree_select|richtext|textarea|checkbox$}
|
when %r{^(input|select|tree_select|richtext|textarea|checkbox)$}
|
||||||
ActiveRecord::Migration.change_column(
|
ActiveRecord::Migration.change_column(
|
||||||
model.table_name,
|
model.table_name,
|
||||||
attribute.name,
|
attribute.name,
|
||||||
|
@ -611,7 +621,21 @@ to send no browser reload event, pass false
|
||||||
limit: attribute.data_option[:maxlength],
|
limit: attribute.data_option[:maxlength],
|
||||||
null: true
|
null: true
|
||||||
)
|
)
|
||||||
when %r{^integer|user_autocompletion|datetime|date$}, %r{^boolean|active$}
|
when 'multiselect'
|
||||||
|
options = {
|
||||||
|
null: true,
|
||||||
|
}
|
||||||
|
if Rails.application.config.db_column_array
|
||||||
|
options[:array] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveRecord::Migration.change_column(
|
||||||
|
model.table_name,
|
||||||
|
attribute.name,
|
||||||
|
data_type,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
when %r{^(integer|user_autocompletion|datetime|date)$}, %r{^(boolean|active)$}
|
||||||
ActiveRecord::Migration.change_column(
|
ActiveRecord::Migration.change_column(
|
||||||
model.table_name,
|
model.table_name,
|
||||||
attribute.name,
|
attribute.name,
|
||||||
|
@ -635,7 +659,7 @@ to send no browser reload event, pass false
|
||||||
|
|
||||||
# create field
|
# create field
|
||||||
case attribute.data_type
|
case attribute.data_type
|
||||||
when %r{^input|select|tree_select|richtext|textarea|checkbox$}
|
when %r{^(input|select|tree_select|richtext|textarea|checkbox)$}
|
||||||
ActiveRecord::Migration.add_column(
|
ActiveRecord::Migration.add_column(
|
||||||
model.table_name,
|
model.table_name,
|
||||||
attribute.name,
|
attribute.name,
|
||||||
|
@ -643,7 +667,21 @@ to send no browser reload event, pass false
|
||||||
limit: attribute.data_option[:maxlength],
|
limit: attribute.data_option[:maxlength],
|
||||||
null: true
|
null: true
|
||||||
)
|
)
|
||||||
when %r{^integer|user_autocompletion$}, %r{^boolean|active$}, %r{^datetime|date$}
|
when 'multiselect'
|
||||||
|
options = {
|
||||||
|
null: true,
|
||||||
|
}
|
||||||
|
if Rails.application.config.db_column_array
|
||||||
|
options[:array] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveRecord::Migration.add_column(
|
||||||
|
model.table_name,
|
||||||
|
attribute.name,
|
||||||
|
data_type,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
when %r{^(integer|user_autocompletion)$}, %r{^(boolean|active)$}, %r{^(datetime|date)$}
|
||||||
ActiveRecord::Migration.add_column(
|
ActiveRecord::Migration.add_column(
|
||||||
model.table_name,
|
model.table_name,
|
||||||
attribute.name,
|
attribute.name,
|
||||||
|
@ -866,7 +904,7 @@ is certain attribute used by triggers, overviews or schedulers
|
||||||
local_data_option[:null] = true if local_data_option[:null].nil?
|
local_data_option[:null] = true if local_data_option[:null].nil?
|
||||||
|
|
||||||
case data_type
|
case data_type
|
||||||
when %r{^((tree_)?select|checkbox)$}
|
when %r{^((multi|tree_)?select|checkbox)$}
|
||||||
local_data_option[:nulloption] = true if local_data_option[:nulloption].nil?
|
local_data_option[:nulloption] = true if local_data_option[:nulloption].nil?
|
||||||
local_data_option[:maxlength] ||= 255
|
local_data_option[:maxlength] ||= 255
|
||||||
end
|
end
|
||||||
|
@ -889,7 +927,7 @@ is certain attribute used by triggers, overviews or schedulers
|
||||||
end
|
end
|
||||||
|
|
||||||
def data_type_must_not_change
|
def data_type_must_not_change
|
||||||
allowable_changes = %w[tree_select select input checkbox]
|
allowable_changes = %w[tree_select select multiselect input checkbox]
|
||||||
|
|
||||||
return if !data_type_changed?
|
return if !data_type_changed?
|
||||||
return if (data_type_change - allowable_changes).empty?
|
return if (data_type_change - allowable_changes).empty?
|
||||||
|
@ -961,7 +999,7 @@ is certain attribute used by triggers, overviews or schedulers
|
||||||
data_option_maxlength_check
|
data_option_maxlength_check
|
||||||
when 'integer'
|
when 'integer'
|
||||||
data_option_min_max_check
|
data_option_min_max_check
|
||||||
when %r{^((tree_)?select|checkbox)$}
|
when %r{^((multi|tree_)?select|checkbox)$}
|
||||||
data_option_default_check + data_option_relation_check
|
data_option_default_check + data_option_relation_check
|
||||||
when 'boolean'
|
when 'boolean'
|
||||||
data_option_default_check + data_option_nil_check
|
data_option_default_check + data_option_nil_check
|
||||||
|
@ -971,4 +1009,11 @@ is certain attribute used by triggers, overviews or schedulers
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_multiselect
|
||||||
|
return if data_type != 'multiselect'
|
||||||
|
return if data_option && data_option[:multiple] == true
|
||||||
|
|
||||||
|
data_option[:multiple] = true
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -684,7 +684,6 @@ condition example
|
||||||
end
|
end
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
|
|
||||||
if selector['operator'] == 'is'
|
if selector['operator'] == 'is'
|
||||||
if selector['pre_condition'] == 'not_set'
|
if selector['pre_condition'] == 'not_set'
|
||||||
if attributes[1].match?(%r{^(created_by|updated_by|owner|customer|user)_id})
|
if attributes[1].match?(%r{^(created_by|updated_by|owner|customer|user)_id})
|
||||||
|
@ -779,65 +778,81 @@ condition example
|
||||||
query += "#{attribute} NOT #{like} (?)"
|
query += "#{attribute} NOT #{like} (?)"
|
||||||
value = "%#{selector['value']}%"
|
value = "%#{selector['value']}%"
|
||||||
bind_params.push value
|
bind_params.push value
|
||||||
elsif selector['operator'] == 'contains all' && attributes[0] == 'ticket' && attributes[1] == 'tags'
|
elsif selector['operator'] == 'contains all'
|
||||||
query += "? = (
|
if attributes[0] == 'ticket' && attributes[1] == 'tags'
|
||||||
SELECT
|
query += "? = (
|
||||||
COUNT(*)
|
SELECT
|
||||||
FROM
|
COUNT(*)
|
||||||
tag_objects,
|
FROM
|
||||||
tag_items,
|
tag_objects,
|
||||||
tags
|
tag_items,
|
||||||
WHERE
|
tags
|
||||||
tickets.id = tags.o_id AND
|
WHERE
|
||||||
tag_objects.id = tags.tag_object_id AND
|
tickets.id = tags.o_id AND
|
||||||
tag_objects.name = 'Ticket' AND
|
tag_objects.id = tags.tag_object_id AND
|
||||||
tag_items.id = tags.tag_item_id AND
|
tag_objects.name = 'Ticket' AND
|
||||||
tag_items.name IN (?)
|
tag_items.id = tags.tag_item_id AND
|
||||||
)"
|
tag_items.name IN (?)
|
||||||
bind_params.push selector['value'].count
|
)"
|
||||||
bind_params.push selector['value']
|
bind_params.push selector['value'].count
|
||||||
elsif selector['operator'] == 'contains one' && attributes[0] == 'ticket' && attributes[1] == 'tags'
|
bind_params.push selector['value']
|
||||||
tables += ', tag_objects, tag_items, tags'
|
elsif Ticket.column_names.include?(attributes[1])
|
||||||
query += "
|
query += SqlHelper.new(object: Ticket).array_contains_all(attributes[1], selector['value'])
|
||||||
tickets.id = tags.o_id AND
|
end
|
||||||
tag_objects.id = tags.tag_object_id AND
|
elsif selector['operator'] == 'contains one' && attributes[0] == 'ticket'
|
||||||
tag_objects.name = 'Ticket' AND
|
if attributes[1] == 'tags'
|
||||||
tag_items.id = tags.tag_item_id AND
|
tables += ', tag_objects, tag_items, tags'
|
||||||
tag_items.name IN (?)"
|
query += "
|
||||||
|
tickets.id = tags.o_id AND
|
||||||
|
tag_objects.id = tags.tag_object_id AND
|
||||||
|
tag_objects.name = 'Ticket' AND
|
||||||
|
tag_items.id = tags.tag_item_id AND
|
||||||
|
tag_items.name IN (?)"
|
||||||
|
|
||||||
bind_params.push selector['value']
|
bind_params.push selector['value']
|
||||||
elsif selector['operator'] == 'contains all not' && attributes[0] == 'ticket' && attributes[1] == 'tags'
|
elsif Ticket.column_names.include?(attributes[1])
|
||||||
query += "0 = (
|
query += SqlHelper.new(object: Ticket).array_contains_one(attributes[1], selector['value'])
|
||||||
SELECT
|
end
|
||||||
COUNT(*)
|
elsif selector['operator'] == 'contains all not' && attributes[0] == 'ticket'
|
||||||
FROM
|
if attributes[1] == 'tags'
|
||||||
tag_objects,
|
query += "0 = (
|
||||||
tag_items,
|
SELECT
|
||||||
tags
|
COUNT(*)
|
||||||
WHERE
|
FROM
|
||||||
tickets.id = tags.o_id AND
|
tag_objects,
|
||||||
tag_objects.id = tags.tag_object_id AND
|
tag_items,
|
||||||
tag_objects.name = 'Ticket' AND
|
tags
|
||||||
tag_items.id = tags.tag_item_id AND
|
WHERE
|
||||||
tag_items.name IN (?)
|
tickets.id = tags.o_id AND
|
||||||
)"
|
tag_objects.id = tags.tag_object_id AND
|
||||||
bind_params.push selector['value']
|
tag_objects.name = 'Ticket' AND
|
||||||
elsif selector['operator'] == 'contains one not' && attributes[0] == 'ticket' && attributes[1] == 'tags'
|
tag_items.id = tags.tag_item_id AND
|
||||||
query += "(
|
tag_items.name IN (?)
|
||||||
SELECT
|
)"
|
||||||
COUNT(*)
|
bind_params.push selector['value']
|
||||||
FROM
|
elsif Ticket.column_names.include?(attributes[1])
|
||||||
tag_objects,
|
query += SqlHelper.new(object: Ticket).array_contains_all(attributes[1], selector['value'], negated: true)
|
||||||
tag_items,
|
end
|
||||||
tags
|
elsif selector['operator'] == 'contains one not' && attributes[0] == 'ticket'
|
||||||
WHERE
|
if attributes[1] == 'tags'
|
||||||
tickets.id = tags.o_id AND
|
query += "(
|
||||||
tag_objects.id = tags.tag_object_id AND
|
SELECT
|
||||||
tag_objects.name = 'Ticket' AND
|
COUNT(*)
|
||||||
tag_items.id = tags.tag_item_id AND
|
FROM
|
||||||
tag_items.name IN (?)
|
tag_objects,
|
||||||
) BETWEEN 0 AND 0"
|
tag_items,
|
||||||
bind_params.push selector['value']
|
tags
|
||||||
|
WHERE
|
||||||
|
tickets.id = tags.o_id AND
|
||||||
|
tag_objects.id = tags.tag_object_id AND
|
||||||
|
tag_objects.name = 'Ticket' AND
|
||||||
|
tag_items.id = tags.tag_item_id AND
|
||||||
|
tag_items.name IN (?)
|
||||||
|
) BETWEEN 0 AND 0"
|
||||||
|
bind_params.push selector['value']
|
||||||
|
elsif Ticket.column_names.include?(attributes[1])
|
||||||
|
query += SqlHelper.new(object: Ticket).array_contains_one(attributes[1], selector['value'], negated: true)
|
||||||
|
end
|
||||||
elsif selector['operator'] == 'before (absolute)'
|
elsif selector['operator'] == 'before (absolute)'
|
||||||
query += "#{attribute} <= ?"
|
query += "#{attribute} <= ?"
|
||||||
bind_params.push selector['value']
|
bind_params.push selector['value']
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
case ActiveRecord::Base.connection_config[:adapter]
|
case ActiveRecord::Base.connection_config[:adapter]
|
||||||
when 'mysql2'
|
when 'mysql2'
|
||||||
Rails.application.config.db_4bytes_utf8 = false
|
Rails.application.config.db_4bytes_utf8 = false
|
||||||
|
Rails.application.config.db_column_array = false
|
||||||
Rails.application.config.db_case_sensitive = false
|
Rails.application.config.db_case_sensitive = false
|
||||||
Rails.application.config.db_like = 'LIKE'
|
Rails.application.config.db_like = 'LIKE'
|
||||||
Rails.application.config.db_null_byte = true
|
Rails.application.config.db_null_byte = true
|
||||||
|
@ -15,6 +16,7 @@ when 'mysql2'
|
||||||
end
|
end
|
||||||
when 'postgresql'
|
when 'postgresql'
|
||||||
Rails.application.config.db_4bytes_utf8 = true
|
Rails.application.config.db_4bytes_utf8 = true
|
||||||
|
Rails.application.config.db_column_array = true
|
||||||
Rails.application.config.db_case_sensitive = true
|
Rails.application.config.db_case_sensitive = true
|
||||||
Rails.application.config.db_like = 'ILIKE'
|
Rails.application.config.db_like = 'ILIKE'
|
||||||
Rails.application.config.db_null_byte = false
|
Rails.application.config.db_null_byte = false
|
||||||
|
|
|
@ -433,6 +433,7 @@ msgstr ""
|
||||||
#: app/assets/javascripts/app/views/integration/ldap_wizard.jst.eco
|
#: app/assets/javascripts/app/views/integration/ldap_wizard.jst.eco
|
||||||
#: app/assets/javascripts/app/views/integration/placetel.jst.eco
|
#: app/assets/javascripts/app/views/integration/placetel.jst.eco
|
||||||
#: app/assets/javascripts/app/views/integration/sipgate.jst.eco
|
#: app/assets/javascripts/app/views/integration/sipgate.jst.eco
|
||||||
|
#: app/assets/javascripts/app/views/object_manager/attribute/multiselect.jst.eco
|
||||||
#: app/assets/javascripts/app/views/object_manager/attribute/select.jst.eco
|
#: app/assets/javascripts/app/views/object_manager/attribute/select.jst.eco
|
||||||
#: app/assets/javascripts/app/views/object_manager/attribute/tree_select.jst.eco
|
#: app/assets/javascripts/app/views/object_manager/attribute/tree_select.jst.eco
|
||||||
#: app/assets/javascripts/app/views/object_manager/index.jst.eco
|
#: app/assets/javascripts/app/views/object_manager/index.jst.eco
|
||||||
|
@ -516,6 +517,7 @@ msgstr ""
|
||||||
#: app/assets/javascripts/app/views/layout_ref/scheduler_modal.jst.eco
|
#: app/assets/javascripts/app/views/layout_ref/scheduler_modal.jst.eco
|
||||||
#: app/assets/javascripts/app/views/layout_ref/sla_modal.jst.eco
|
#: app/assets/javascripts/app/views/layout_ref/sla_modal.jst.eco
|
||||||
#: app/assets/javascripts/app/views/microsoft365/list.jst.eco
|
#: app/assets/javascripts/app/views/microsoft365/list.jst.eco
|
||||||
|
#: app/assets/javascripts/app/views/object_manager/attribute/multiselect.jst.eco
|
||||||
#: app/assets/javascripts/app/views/object_manager/attribute/select.jst.eco
|
#: app/assets/javascripts/app/views/object_manager/attribute/select.jst.eco
|
||||||
#: app/assets/javascripts/app/views/profile/linked_accounts.jst.eco
|
#: app/assets/javascripts/app/views/profile/linked_accounts.jst.eco
|
||||||
#: app/assets/javascripts/app/views/tag/index.jst.eco
|
#: app/assets/javascripts/app/views/tag/index.jst.eco
|
||||||
|
@ -1594,6 +1596,11 @@ msgstr ""
|
||||||
msgid "Check the response and payload for detailed information:"
|
msgid "Check the response and payload for detailed information:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/assets/javascripts/app/views/object_manager/attribute/multiselect.jst.eco
|
||||||
|
#: app/assets/javascripts/app/views/object_manager/attribute/select.jst.eco
|
||||||
|
msgid "Check this box if you want to customise how options are been sorted. If checkbox is disabled, values are sorted in alphabetical order."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/controllers/_integration/check_mk.coffee
|
#: app/assets/javascripts/app/controllers/_integration/check_mk.coffee
|
||||||
msgid "Checkmk"
|
msgid "Checkmk"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -2418,6 +2425,7 @@ msgstr ""
|
||||||
#: app/assets/javascripts/app/views/channel/chat.jst.eco
|
#: app/assets/javascripts/app/views/channel/chat.jst.eco
|
||||||
#: app/assets/javascripts/app/views/generic/multi_locales.jst.eco
|
#: app/assets/javascripts/app/views/generic/multi_locales.jst.eco
|
||||||
#: app/assets/javascripts/app/views/object_manager/attribute/boolean.jst.eco
|
#: app/assets/javascripts/app/views/object_manager/attribute/boolean.jst.eco
|
||||||
|
#: app/assets/javascripts/app/views/object_manager/attribute/multiselect.jst.eco
|
||||||
#: app/assets/javascripts/app/views/object_manager/attribute/select.jst.eco
|
#: app/assets/javascripts/app/views/object_manager/attribute/select.jst.eco
|
||||||
#: db/seeds/settings.rb
|
#: db/seeds/settings.rb
|
||||||
msgid "Default"
|
msgid "Default"
|
||||||
|
@ -3220,6 +3228,7 @@ msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/models/object_manager_attribute.coffee
|
#: app/assets/javascripts/app/models/object_manager_attribute.coffee
|
||||||
#: app/assets/javascripts/app/views/object_manager/attribute/boolean.jst.eco
|
#: app/assets/javascripts/app/views/object_manager/attribute/boolean.jst.eco
|
||||||
|
#: app/assets/javascripts/app/views/object_manager/attribute/multiselect.jst.eco
|
||||||
#: app/assets/javascripts/app/views/object_manager/attribute/select.jst.eco
|
#: app/assets/javascripts/app/views/object_manager/attribute/select.jst.eco
|
||||||
#: app/assets/javascripts/app/views/object_manager/index.jst.eco
|
#: app/assets/javascripts/app/views/object_manager/index.jst.eco
|
||||||
msgid "Display"
|
msgid "Display"
|
||||||
|
@ -5283,6 +5292,7 @@ msgid "Keep messages on server"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/views/object_manager/attribute/boolean.jst.eco
|
#: app/assets/javascripts/app/views/object_manager/attribute/boolean.jst.eco
|
||||||
|
#: app/assets/javascripts/app/views/object_manager/attribute/multiselect.jst.eco
|
||||||
#: app/assets/javascripts/app/views/object_manager/attribute/select.jst.eco
|
#: app/assets/javascripts/app/views/object_manager/attribute/select.jst.eco
|
||||||
#: app/assets/javascripts/app/views/object_manager/attribute/tree_select.jst.eco
|
#: app/assets/javascripts/app/views/object_manager/attribute/tree_select.jst.eco
|
||||||
msgid "Key"
|
msgid "Key"
|
||||||
|
@ -5981,6 +5991,10 @@ msgstr ""
|
||||||
msgid "Moved out"
|
msgid "Moved out"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee
|
||||||
|
msgid "Multiselect"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: db/seeds/overviews.rb
|
#: db/seeds/overviews.rb
|
||||||
msgid "My Organization Tickets"
|
msgid "My Organization Tickets"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -7498,6 +7512,7 @@ msgstr ""
|
||||||
#: app/assets/javascripts/app/views/integration/placetel.jst.eco
|
#: app/assets/javascripts/app/views/integration/placetel.jst.eco
|
||||||
#: app/assets/javascripts/app/views/integration/sipgate.jst.eco
|
#: app/assets/javascripts/app/views/integration/sipgate.jst.eco
|
||||||
#: app/assets/javascripts/app/views/navigation/menu_cti_ringing.jst.eco
|
#: app/assets/javascripts/app/views/navigation/menu_cti_ringing.jst.eco
|
||||||
|
#: app/assets/javascripts/app/views/object_manager/attribute/multiselect.jst.eco
|
||||||
#: app/assets/javascripts/app/views/object_manager/attribute/select.jst.eco
|
#: app/assets/javascripts/app/views/object_manager/attribute/select.jst.eco
|
||||||
#: app/assets/javascripts/app/views/profile/devices.jst.eco
|
#: app/assets/javascripts/app/views/profile/devices.jst.eco
|
||||||
#: app/assets/javascripts/app/views/twitter/search_term.jst.eco
|
#: app/assets/javascripts/app/views/twitter/search_term.jst.eco
|
||||||
|
@ -9839,6 +9854,11 @@ msgstr ""
|
||||||
msgid "Use client storage to cache data to enhance performance of application."
|
msgid "Use client storage to cache data to enhance performance of application."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: app/assets/javascripts/app/views/object_manager/attribute/multiselect.jst.eco
|
||||||
|
#: app/assets/javascripts/app/views/object_manager/attribute/select.jst.eco
|
||||||
|
msgid "Use custom option sort"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/models/application.coffee
|
#: app/assets/javascripts/app/models/application.coffee
|
||||||
msgid "Use one line per URI"
|
msgid "Use one line per URI"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -10830,18 +10850,22 @@ msgid "connected"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
|
#: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
|
||||||
|
#: app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee
|
||||||
msgid "contains"
|
msgid "contains"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
|
#: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
|
||||||
|
#: app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee
|
||||||
msgid "contains all"
|
msgid "contains all"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
|
#: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
|
||||||
|
#: app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee
|
||||||
msgid "contains all not"
|
msgid "contains all not"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
|
#: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
|
||||||
|
#: app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee
|
||||||
msgid "contains not"
|
msgid "contains not"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -11015,6 +11039,7 @@ msgid "h"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
|
#: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
|
||||||
|
#: app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee
|
||||||
#: app/assets/javascripts/app/views/object_manager/index.jst.eco
|
#: app/assets/javascripts/app/views/object_manager/index.jst.eco
|
||||||
msgid "has changed"
|
msgid "has changed"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
|
@ -199,14 +199,19 @@ examples how to use
|
||||||
|
|
||||||
def display_value(object, method_name, previous_method_names, key)
|
def display_value(object, method_name, previous_method_names, key)
|
||||||
return key if method_name != 'value' ||
|
return key if method_name != 'value' ||
|
||||||
!key.instance_of?(String)
|
(!key.instance_of?(String) && !key.instance_of?(Array))
|
||||||
|
|
||||||
attributes = ObjectManager::Attribute
|
attributes = ObjectManager::Attribute
|
||||||
.where(object_lookup_id: ObjectLookup.by_name(object.class.to_s))
|
.where(object_lookup_id: ObjectLookup.by_name(object.class.to_s))
|
||||||
.where(name: previous_method_names.split('.').last)
|
.where(name: previous_method_names.split('.').last)
|
||||||
|
|
||||||
return key if attributes.count.zero? || attributes.first.data_type != 'select'
|
case attributes.first.data_type
|
||||||
|
when 'select'
|
||||||
attributes.first.data_option['options'][key] || key
|
attributes.first.data_option['options'][key] || key
|
||||||
|
when 'multiselect'
|
||||||
|
key.map { |k| attributes.first.data_option['options'][k] || k }
|
||||||
|
else
|
||||||
|
key
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,6 +6,14 @@ class SqlHelper
|
||||||
@object = object
|
@object = object
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def db_column(column)
|
||||||
|
"#{ActiveRecord::Base.connection.quote_table_name(@object.table_name)}.#{ActiveRecord::Base.connection.quote_column_name(column)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def db_value(value)
|
||||||
|
ActiveRecord::Base.connection.quote_string(value)
|
||||||
|
end
|
||||||
|
|
||||||
def get_param_key(key, params)
|
def get_param_key(key, params)
|
||||||
sort_by = []
|
sort_by = []
|
||||||
if params[key].present? && params[key].is_a?(String)
|
if params[key].present? && params[key].is_a?(String)
|
||||||
|
@ -97,7 +105,7 @@ order_by = [
|
||||||
|
|
||||||
def set_sql_order_default(sql, default)
|
def set_sql_order_default(sql, default)
|
||||||
if sql.blank? && default.present?
|
if sql.blank? && default.present?
|
||||||
sql.push("#{ActiveRecord::Base.connection.quote_table_name(@object.table_name)}.#{ActiveRecord::Base.connection.quote_column_name(default)}")
|
sql.push(db_column(default))
|
||||||
end
|
end
|
||||||
sql
|
sql
|
||||||
end
|
end
|
||||||
|
@ -128,7 +136,7 @@ sql = 'tickets.created_at, tickets.updated_at'
|
||||||
next if value.blank?
|
next if value.blank?
|
||||||
next if order_by[index].blank?
|
next if order_by[index].blank?
|
||||||
|
|
||||||
sql.push("#{ActiveRecord::Base.connection.quote_table_name(@object.table_name)}.#{ActiveRecord::Base.connection.quote_column_name(value)}")
|
sql.push(db_column(value))
|
||||||
end
|
end
|
||||||
|
|
||||||
sql = set_sql_order_default(sql, default)
|
sql = set_sql_order_default(sql, default)
|
||||||
|
@ -162,11 +170,34 @@ sql = 'tickets.created_at ASC, tickets.updated_at DESC'
|
||||||
next if value.blank?
|
next if value.blank?
|
||||||
next if order_by[index].blank?
|
next if order_by[index].blank?
|
||||||
|
|
||||||
sql.push("#{ActiveRecord::Base.connection.quote_table_name(@object.table_name)}.#{ActiveRecord::Base.connection.quote_column_name(value)} #{order_by[index]}")
|
sql.push("#{db_column(value)} #{order_by[index]}")
|
||||||
end
|
end
|
||||||
|
|
||||||
sql = set_sql_order_default(sql, default)
|
sql = set_sql_order_default(sql, default)
|
||||||
|
|
||||||
sql.join(', ')
|
sql.join(', ')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def array_contains_all(attribute, value, negated: false)
|
||||||
|
value = [''] if value.blank?
|
||||||
|
value = Array(value)
|
||||||
|
result = if Rails.application.config.db_column_array
|
||||||
|
"(#{db_column(attribute)} @> ARRAY[#{value.map { |v| "'#{db_value(v)}'" }.join(',')}]::varchar[])"
|
||||||
|
else
|
||||||
|
"JSON_CONTAINS(#{db_column(attribute)}, '#{db_value(value.to_json)}', '$')"
|
||||||
|
end
|
||||||
|
negated ? "NOT(#{result})" : "(#{result})"
|
||||||
|
end
|
||||||
|
|
||||||
|
def array_contains_one(attribute, value, negated: false)
|
||||||
|
value = [''] if value.blank?
|
||||||
|
value = Array(value)
|
||||||
|
result = if Rails.application.config.db_column_array
|
||||||
|
"(#{db_column(attribute)} && ARRAY[#{value.map { |v| "'#{db_value(v)}'" }.join(',')}]::varchar[])"
|
||||||
|
else
|
||||||
|
value.map { |v| "JSON_CONTAINS(#{db_column(attribute)}, '#{db_value(v.to_json)}', '$')" }.join(' OR ')
|
||||||
|
end
|
||||||
|
negated ? "NOT(#{result})" : "(#{result})"
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -32,6 +32,13 @@ RSpec.describe CheckForObjectAttributes, type: :db_migration do
|
||||||
expect { migrate }
|
expect { migrate }
|
||||||
.not_to change { attribute.reload.data_option }
|
.not_to change { attribute.reload.data_option }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not change multiselect attribute' do
|
||||||
|
attribute = create(:object_manager_attribute_multiselect)
|
||||||
|
|
||||||
|
expect { migrate }
|
||||||
|
.not_to change { attribute.reload.data_option }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'for #data_option key:' do
|
context 'for #data_option key:' do
|
||||||
|
|
|
@ -156,6 +156,28 @@ FactoryBot.define do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
factory :object_manager_attribute_multiselect, parent: :object_manager_attribute do
|
||||||
|
default { '' }
|
||||||
|
|
||||||
|
data_type { 'multiselect' }
|
||||||
|
data_option do
|
||||||
|
{
|
||||||
|
'default' => default,
|
||||||
|
'options' => {
|
||||||
|
'key_1' => 'value_1',
|
||||||
|
'key_2' => 'value_2',
|
||||||
|
'key_3' => 'value_3',
|
||||||
|
},
|
||||||
|
'relation' => '',
|
||||||
|
'nulloption' => true,
|
||||||
|
'multiple' => true,
|
||||||
|
'null' => true,
|
||||||
|
'translate' => true,
|
||||||
|
'maxlength' => 255
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
factory :object_manager_attribute_tree_select, parent: :object_manager_attribute do
|
factory :object_manager_attribute_tree_select, parent: :object_manager_attribute do
|
||||||
default { '' }
|
default { '' }
|
||||||
|
|
||||||
|
|
|
@ -60,84 +60,250 @@ RSpec.describe NotificationFactory::Renderer do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when handling ObjectManager::Attribute usage', db_strategy: :reset do
|
context 'when handling ObjectManager::Attribute usage', db_strategy: :reset do
|
||||||
|
before do
|
||||||
it 'correctly renders simple select attributes' do
|
create_object_manager_attribute
|
||||||
create :object_manager_attribute_select, name: 'select'
|
|
||||||
ObjectManager::Attribute.migration_execute
|
ObjectManager::Attribute.migration_execute
|
||||||
|
|
||||||
ticket = create :ticket, customer: @user, select: 'key_1'
|
|
||||||
|
|
||||||
renderer = build :notification_factory_renderer,
|
|
||||||
objects: { ticket: ticket },
|
|
||||||
template: '#{ticket.select} _SEPERATOR_ #{ticket.select.value}'
|
|
||||||
|
|
||||||
expect(renderer.render).to eq 'key_1 _SEPERATOR_ value_1'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'correctly renders select attributes on chained user object' do
|
let(:renderer) do
|
||||||
create :object_manager_attribute_select,
|
build :notification_factory_renderer,
|
||||||
object_lookup_id: ObjectLookup.by_name('User'),
|
objects: { ticket: ticket },
|
||||||
name: 'select'
|
template: template
|
||||||
ObjectManager::Attribute.migration_execute
|
|
||||||
|
|
||||||
user = User.where(firstname: 'Nicole').first
|
|
||||||
user.select = 'key_2'
|
|
||||||
user.save
|
|
||||||
ticket = create :ticket, customer: user
|
|
||||||
|
|
||||||
renderer = build :notification_factory_renderer,
|
|
||||||
objects: { ticket: ticket },
|
|
||||||
template: '#{ticket.customer.select} _SEPERATOR_ #{ticket.customer.select.value}'
|
|
||||||
|
|
||||||
expect(renderer.render).to eq 'key_2 _SEPERATOR_ value_2'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'correctly renders select attributes on chained group object' do
|
shared_examples 'correctly rendering the attributes' do
|
||||||
create :object_manager_attribute_select,
|
it 'correctly renders the attributes' do
|
||||||
object_lookup_id: ObjectLookup.by_name('Group'),
|
expect(renderer.render).to eq expected_render
|
||||||
name: 'select'
|
end
|
||||||
ObjectManager::Attribute.migration_execute
|
|
||||||
|
|
||||||
ticket = create :ticket, customer: @user
|
|
||||||
group = ticket.group
|
|
||||||
group.select = 'key_3'
|
|
||||||
group.save
|
|
||||||
|
|
||||||
renderer = build :notification_factory_renderer,
|
|
||||||
objects: { ticket: ticket },
|
|
||||||
template: '#{ticket.group.select} _SEPERATOR_ #{ticket.group.select.value}'
|
|
||||||
|
|
||||||
expect(renderer.render).to eq 'key_3 _SEPERATOR_ value_3'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'correctly renders select attributes on chained organization object' do
|
context 'with a simple select attribute' do
|
||||||
create :object_manager_attribute_select,
|
let(:create_object_manager_attribute) do
|
||||||
object_lookup_id: ObjectLookup.by_name('Organization'),
|
create :object_manager_attribute_select, name: 'select'
|
||||||
name: 'select'
|
end
|
||||||
ObjectManager::Attribute.migration_execute
|
let(:ticket) { create :ticket, customer: @user, select: 'key_1' }
|
||||||
|
let(:template) { '#{ticket.select} _SEPERATOR_ #{ticket.select.value}' }
|
||||||
|
let(:expected_render) { 'key_1 _SEPERATOR_ value_1' }
|
||||||
|
|
||||||
@user.organization.select = 'key_2'
|
it_behaves_like 'correctly rendering the attributes'
|
||||||
@user.organization.save
|
|
||||||
ticket = create :ticket, customer: @user
|
|
||||||
|
|
||||||
renderer = build :notification_factory_renderer,
|
|
||||||
objects: { ticket: ticket },
|
|
||||||
template: '#{ticket.customer.organization.select} _SEPERATOR_ #{ticket.customer.organization.select.value}'
|
|
||||||
|
|
||||||
expect(renderer.render).to eq 'key_2 _SEPERATOR_ value_2'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'correctly renders tree select attributes' do
|
context 'with select attribute on chained user object' do
|
||||||
create :object_manager_attribute_tree_select, name: 'tree_select'
|
let(:create_object_manager_attribute) do
|
||||||
ObjectManager::Attribute.migration_execute
|
create :object_manager_attribute_select,
|
||||||
|
object_lookup_id: ObjectLookup.by_name('User'),
|
||||||
|
name: 'select'
|
||||||
|
end
|
||||||
|
|
||||||
ticket = create :ticket, customer: @user, tree_select: 'Incident::Hardware::Laptop'
|
let(:user) do
|
||||||
|
user = User.where(firstname: 'Nicole').first
|
||||||
|
user.select = 'key_2'
|
||||||
|
user.save
|
||||||
|
user
|
||||||
|
end
|
||||||
|
|
||||||
renderer = build :notification_factory_renderer,
|
let(:ticket) { create :ticket, customer: user }
|
||||||
objects: { ticket: ticket },
|
let(:template) { '#{ticket.customer.select} _SEPERATOR_ #{ticket.customer.select.value}' }
|
||||||
template: '#{ticket.tree_select} _SEPERATOR_ #{ticket.tree_select.value}'
|
let(:expected_render) { 'key_2 _SEPERATOR_ value_2' }
|
||||||
|
|
||||||
expect(renderer.render).to eq 'Incident::Hardware::Laptop _SEPERATOR_ Incident::Hardware::Laptop'
|
it_behaves_like 'correctly rendering the attributes'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with select attribute on chained group object' do
|
||||||
|
let(:create_object_manager_attribute) do
|
||||||
|
create :object_manager_attribute_select,
|
||||||
|
object_lookup_id: ObjectLookup.by_name('Group'),
|
||||||
|
name: 'select'
|
||||||
|
end
|
||||||
|
let(:template) { '#{ticket.group.select} _SEPERATOR_ #{ticket.group.select.value}' }
|
||||||
|
let(:expected_render) { 'key_3 _SEPERATOR_ value_3' }
|
||||||
|
|
||||||
|
let(:ticket) { create :ticket, customer: @user }
|
||||||
|
|
||||||
|
before do
|
||||||
|
group = ticket.group
|
||||||
|
group.select = 'key_3'
|
||||||
|
group.save
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'correctly rendering the attributes'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with select attribute on chained organization object' do
|
||||||
|
let(:create_object_manager_attribute) do
|
||||||
|
create :object_manager_attribute_select,
|
||||||
|
object_lookup_id: ObjectLookup.by_name('Organization'),
|
||||||
|
name: 'select'
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:user) do
|
||||||
|
@user.organization.select = 'key_2'
|
||||||
|
@user.organization.save
|
||||||
|
@user
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:ticket) { create :ticket, customer: user }
|
||||||
|
let(:template) { '#{ticket.customer.organization.select} _SEPERATOR_ #{ticket.customer.organization.select.value}' }
|
||||||
|
let(:expected_render) { 'key_2 _SEPERATOR_ value_2' }
|
||||||
|
|
||||||
|
it_behaves_like 'correctly rendering the attributes'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with multiselect' do
|
||||||
|
context 'with a simple multiselect attribute' do
|
||||||
|
let(:create_object_manager_attribute) do
|
||||||
|
create :object_manager_attribute_multiselect, name: 'multiselect'
|
||||||
|
end
|
||||||
|
let(:ticket) { create :ticket, customer: @user, multiselect: ['key_1'] }
|
||||||
|
let(:template) { '#{ticket.multiselect} _SEPERATOR_ #{ticket.multiselect.value}' }
|
||||||
|
let(:expected_render) { 'key_1 _SEPERATOR_ value_1' }
|
||||||
|
|
||||||
|
it_behaves_like 'correctly rendering the attributes'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with single multiselect attribute on chained user object' do
|
||||||
|
let(:create_object_manager_attribute) do
|
||||||
|
create :object_manager_attribute_multiselect,
|
||||||
|
object_lookup_id: ObjectLookup.by_name('User'),
|
||||||
|
name: 'multiselect'
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:user) do
|
||||||
|
user = User.where(firstname: 'Nicole').first
|
||||||
|
user.multiselect = ['key_2']
|
||||||
|
user.save
|
||||||
|
user
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:ticket) { create :ticket, customer: user }
|
||||||
|
let(:template) { '#{ticket.customer.multiselect} _SEPERATOR_ #{ticket.customer.multiselect.value}' }
|
||||||
|
let(:expected_render) { 'key_2 _SEPERATOR_ value_2' }
|
||||||
|
|
||||||
|
it_behaves_like 'correctly rendering the attributes'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with single multiselect attribute on chained group object' do
|
||||||
|
let(:create_object_manager_attribute) do
|
||||||
|
create :object_manager_attribute_multiselect,
|
||||||
|
object_lookup_id: ObjectLookup.by_name('Group'),
|
||||||
|
name: 'multiselect'
|
||||||
|
end
|
||||||
|
let(:template) { '#{ticket.group.multiselect} _SEPERATOR_ #{ticket.group.multiselect.value}' }
|
||||||
|
let(:expected_render) { 'key_3 _SEPERATOR_ value_3' }
|
||||||
|
|
||||||
|
let(:ticket) { create :ticket, customer: @user }
|
||||||
|
|
||||||
|
before do
|
||||||
|
group = ticket.group
|
||||||
|
group.multiselect = ['key_3']
|
||||||
|
group.save
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'correctly rendering the attributes'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with single multiselect attribute on chained organization object' do
|
||||||
|
let(:create_object_manager_attribute) do
|
||||||
|
create :object_manager_attribute_multiselect,
|
||||||
|
object_lookup_id: ObjectLookup.by_name('Organization'),
|
||||||
|
name: 'multiselect'
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:user) do
|
||||||
|
@user.organization.multiselect = ['key_2']
|
||||||
|
@user.organization.save
|
||||||
|
@user
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:ticket) { create :ticket, customer: user }
|
||||||
|
let(:template) { '#{ticket.customer.organization.multiselect} _SEPERATOR_ #{ticket.customer.organization.multiselect.value}' }
|
||||||
|
let(:expected_render) { 'key_2 _SEPERATOR_ value_2' }
|
||||||
|
|
||||||
|
it_behaves_like 'correctly rendering the attributes'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a multiple multiselect attribute' do
|
||||||
|
let(:create_object_manager_attribute) do
|
||||||
|
create :object_manager_attribute_multiselect, name: 'multiselect'
|
||||||
|
end
|
||||||
|
let(:ticket) { create :ticket, customer: @user, multiselect: %w[key_1 key_2] }
|
||||||
|
let(:template) { '#{ticket.multiselect} _SEPERATOR_ #{ticket.multiselect.value}' }
|
||||||
|
let(:expected_render) { 'key_1, key_2 _SEPERATOR_ value_1, value_2' }
|
||||||
|
|
||||||
|
it_behaves_like 'correctly rendering the attributes'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with multiple multiselect attribute on chained user object' do
|
||||||
|
let(:create_object_manager_attribute) do
|
||||||
|
create :object_manager_attribute_multiselect,
|
||||||
|
object_lookup_id: ObjectLookup.by_name('User'),
|
||||||
|
name: 'multiselect'
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:user) do
|
||||||
|
user = User.where(firstname: 'Nicole').first
|
||||||
|
user.multiselect = %w[key_2 key_3]
|
||||||
|
user.save
|
||||||
|
user
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:ticket) { create :ticket, customer: user }
|
||||||
|
let(:template) { '#{ticket.customer.multiselect} _SEPERATOR_ #{ticket.customer.multiselect.value}' }
|
||||||
|
let(:expected_render) { 'key_2, key_3 _SEPERATOR_ value_2, value_3' }
|
||||||
|
|
||||||
|
it_behaves_like 'correctly rendering the attributes'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with multiple multiselect attribute on chained group object' do
|
||||||
|
let(:create_object_manager_attribute) do
|
||||||
|
create :object_manager_attribute_multiselect,
|
||||||
|
object_lookup_id: ObjectLookup.by_name('Group'),
|
||||||
|
name: 'multiselect'
|
||||||
|
end
|
||||||
|
let(:template) { '#{ticket.group.multiselect} _SEPERATOR_ #{ticket.group.multiselect.value}' }
|
||||||
|
let(:expected_render) { 'key_3, key_1 _SEPERATOR_ value_3, value_1' }
|
||||||
|
|
||||||
|
let(:ticket) { create :ticket, customer: @user }
|
||||||
|
|
||||||
|
before do
|
||||||
|
group = ticket.group
|
||||||
|
group.multiselect = %w[key_3 key_1]
|
||||||
|
group.save
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'correctly rendering the attributes'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with multiple multiselect attribute on chained organization object' do
|
||||||
|
let(:create_object_manager_attribute) do
|
||||||
|
create :object_manager_attribute_multiselect,
|
||||||
|
object_lookup_id: ObjectLookup.by_name('Organization'),
|
||||||
|
name: 'multiselect'
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:user) do
|
||||||
|
@user.organization.multiselect = %w[key_2 key_1]
|
||||||
|
@user.organization.save
|
||||||
|
@user
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:ticket) { create :ticket, customer: user }
|
||||||
|
let(:template) { '#{ticket.customer.organization.multiselect} _SEPERATOR_ #{ticket.customer.organization.multiselect.value}' }
|
||||||
|
let(:expected_render) { 'key_2, key_1 _SEPERATOR_ value_2, value_1' }
|
||||||
|
|
||||||
|
it_behaves_like 'correctly rendering the attributes'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a tree select attribute' do
|
||||||
|
let(:create_object_manager_attribute) do
|
||||||
|
create :object_manager_attribute_tree_select, name: 'tree_select'
|
||||||
|
end
|
||||||
|
let(:ticket) { create :ticket, customer: @user, tree_select: 'Incident::Hardware::Laptop' }
|
||||||
|
let(:template) { '#{ticket.tree_select} _SEPERATOR_ #{ticket.tree_select.value}' }
|
||||||
|
let(:expected_render) { 'Incident::Hardware::Laptop _SEPERATOR_ Incident::Hardware::Laptop' }
|
||||||
|
|
||||||
|
it_behaves_like 'correctly rendering the attributes'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -299,6 +299,37 @@ RSpec.describe CoreWorkflow, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.perform - Default - Restrict values for multiselect fields', db_strategy: :reset do
|
||||||
|
let(:field_name) { SecureRandom.uuid }
|
||||||
|
|
||||||
|
before do
|
||||||
|
create :object_manager_attribute_multiselect, name: field_name, display: field_name
|
||||||
|
ObjectManager::Attribute.migration_execute
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without saved values' do
|
||||||
|
it 'does return the correct list of selectable values' do
|
||||||
|
expect(result[:restrict_values][field_name]).to eq(['', 'key_1', 'key_2', 'key_3'])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with saved values' do
|
||||||
|
let(:payload) do
|
||||||
|
base_payload.merge('params' => {
|
||||||
|
'id' => ticket.id,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
ticket.reload.update(field_name.to_sym => %w[key_2 key_3])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does return the correct list of selectable values' do
|
||||||
|
expect(result[:restrict_values][field_name]).to eq(['', 'key_1', 'key_2', 'key_3'])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '.perform - Custom - Pending Time' do
|
describe '.perform - Custom - Pending Time' do
|
||||||
it 'does not show pending time for non pending state' do
|
it 'does not show pending time for non pending state' do
|
||||||
expect(result[:visibility]['pending_time']).to eq('remove')
|
expect(result[:visibility]['pending_time']).to eq('remove')
|
||||||
|
|
|
@ -931,4 +931,220 @@ RSpec.describe Trigger, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'multiselect triggers', db_strategy: :reset do
|
||||||
|
|
||||||
|
let(:attribute_name) { 'multiselect' }
|
||||||
|
|
||||||
|
let(:condition) do
|
||||||
|
{ "ticket.#{attribute_name}" => { 'operator' => operator, 'value' => trigger_values } }
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:perform) do
|
||||||
|
{ 'article.note' => { 'subject' => 'Test subject note', 'internal' => 'true', 'body' => 'Test body note' } }
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
create :object_manager_attribute_multiselect, name: attribute_name
|
||||||
|
ObjectManager::Attribute.migration_execute
|
||||||
|
|
||||||
|
described_class.destroy_all # Default DB state includes three sample triggers
|
||||||
|
trigger # create subject trigger
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when ticket is updated with a multiselect trigger condition', authenticated_as: :owner, db_strategy: :reset do
|
||||||
|
let(:options) do
|
||||||
|
{
|
||||||
|
a: 'a',
|
||||||
|
b: 'b',
|
||||||
|
c: 'c',
|
||||||
|
d: 'd',
|
||||||
|
e: 'e',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:trigger_values) { %w[a b c] }
|
||||||
|
let(:group) { create(:group) }
|
||||||
|
let(:owner) { create(:admin, group_ids: [group.id]) }
|
||||||
|
let!(:ticket) { create(:ticket, group: group,) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
ticket.update_attribute(attribute_name, ticket_multiselect_values)
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'updating the ticket with the trigger condition' do
|
||||||
|
it 'updates the ticket with the trigger condition' do
|
||||||
|
expect { TransactionDispatcher.commit }
|
||||||
|
.to change(Ticket::Article, :count).by(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'not updating the ticket with the trigger condition' do
|
||||||
|
it 'does not update the ticket with the trigger condition' do
|
||||||
|
expect { TransactionDispatcher.commit }
|
||||||
|
.to not_change(Ticket::Article, :count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with 'contains all' used" do
|
||||||
|
let(:operator) { 'contains all' }
|
||||||
|
|
||||||
|
context 'when updated value is the same with trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { trigger_values }
|
||||||
|
|
||||||
|
it_behaves_like 'updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when updated value is different from the trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { options.values - trigger_values }
|
||||||
|
|
||||||
|
it_behaves_like 'not updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when no value is selected' do
|
||||||
|
let(:ticket_multiselect_values) { ['-'] }
|
||||||
|
|
||||||
|
it_behaves_like 'not updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when all value is selected' do
|
||||||
|
let(:ticket_multiselect_values) { options.values }
|
||||||
|
|
||||||
|
it_behaves_like 'updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when updated value contains one of the trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { [trigger_values.first] }
|
||||||
|
|
||||||
|
it_behaves_like 'not updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when updated value does not contain one of the trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { options.values - [trigger_values.first] }
|
||||||
|
|
||||||
|
it_behaves_like 'not updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with 'contains one' used" do
|
||||||
|
let(:operator) { 'contains one' }
|
||||||
|
|
||||||
|
context 'when updated value is the same with trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { trigger_values }
|
||||||
|
|
||||||
|
it_behaves_like 'updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when updated value is different from the trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { options.values - trigger_values }
|
||||||
|
|
||||||
|
it_behaves_like 'not updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when no value is selected' do
|
||||||
|
let(:ticket_multiselect_values) { ['-'] }
|
||||||
|
|
||||||
|
it_behaves_like 'not updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when all value is selected' do
|
||||||
|
let(:ticket_multiselect_values) { options.values }
|
||||||
|
|
||||||
|
it_behaves_like 'updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when updated value contains only one of the trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { [trigger_values.first] }
|
||||||
|
|
||||||
|
it_behaves_like 'updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when updated value does not contain one of the trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { options.values - [trigger_values.first] }
|
||||||
|
|
||||||
|
it_behaves_like 'updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with 'contains all not' used" do
|
||||||
|
let(:operator) { 'contains all not' }
|
||||||
|
|
||||||
|
context 'when updated value is the same with trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { trigger_values }
|
||||||
|
|
||||||
|
it_behaves_like 'not updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when updated value is different from the trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { options.values - trigger_values }
|
||||||
|
|
||||||
|
it_behaves_like 'updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when no value is selected' do
|
||||||
|
let(:ticket_multiselect_values) { ['-'] }
|
||||||
|
|
||||||
|
it_behaves_like 'updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when all value is selected' do
|
||||||
|
let(:ticket_multiselect_values) { options.values }
|
||||||
|
|
||||||
|
it_behaves_like 'not updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when updated value contains only one of the trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { [trigger_values.first] }
|
||||||
|
|
||||||
|
it_behaves_like 'updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when updated value does not contain one of the trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { options.values - [trigger_values.first] }
|
||||||
|
|
||||||
|
it_behaves_like 'updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with 'contains one not' used" do
|
||||||
|
let(:operator) { 'contains one not' }
|
||||||
|
|
||||||
|
context 'when updated value is the same with trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { trigger_values }
|
||||||
|
|
||||||
|
it_behaves_like 'not updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when updated value is different from the trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { options.values - trigger_values }
|
||||||
|
|
||||||
|
it_behaves_like 'updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when no value is selected' do
|
||||||
|
let(:ticket_multiselect_values) { ['-'] }
|
||||||
|
|
||||||
|
it_behaves_like 'updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when all value is selected' do
|
||||||
|
let(:ticket_multiselect_values) { options.values }
|
||||||
|
|
||||||
|
it_behaves_like 'not updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when updated value contains only one of the trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { [trigger_values.first] }
|
||||||
|
|
||||||
|
it_behaves_like 'not updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when updated value does not contain one of the trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { options.values - [trigger_values.first] }
|
||||||
|
|
||||||
|
it_behaves_like 'not updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,4 +16,16 @@ RSpec.configure do |config|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
config.filter_run_excluding db_adapter: lambda { |adapter|
|
||||||
|
adapter_config = ActiveRecord::Base.connection_config[:adapter]
|
||||||
|
case adapter
|
||||||
|
when :postgresql
|
||||||
|
adapter_config != 'postgresql'
|
||||||
|
when :mysql
|
||||||
|
adapter_config != 'mysql2'
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
|
@ -619,6 +619,206 @@ RSpec.shared_examples 'core workflow' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'modify multiselect attribute', authenticated_as: :authenticate, db_strategy: :reset do
|
||||||
|
def authenticate
|
||||||
|
create(:object_manager_attribute_multiselect, object_name: object_name, name: field_name, display: field_name, screens: screens)
|
||||||
|
ObjectManager::Attribute.migration_execute
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'action - show' do
|
||||||
|
before do
|
||||||
|
create(:core_workflow,
|
||||||
|
object: object_name,
|
||||||
|
perform: {
|
||||||
|
"#{object_name.downcase}.#{field_name}": {
|
||||||
|
operator: 'show',
|
||||||
|
show: 'true'
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does perform' do
|
||||||
|
before_it.call
|
||||||
|
expect(page).to have_selector("select[name='#{field_name}']", wait: 10)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'action - hide' do
|
||||||
|
before do
|
||||||
|
create(:core_workflow,
|
||||||
|
object: object_name,
|
||||||
|
perform: {
|
||||||
|
"#{object_name.downcase}.#{field_name}": {
|
||||||
|
operator: 'hide',
|
||||||
|
hide: 'true'
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does perform' do
|
||||||
|
before_it.call
|
||||||
|
expect(page).to have_selector(".form-group[data-attribute-name='#{field_name}'].is-hidden", visible: :hidden, wait: 10)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'action - remove' do
|
||||||
|
before do
|
||||||
|
create(:core_workflow,
|
||||||
|
object: object_name,
|
||||||
|
perform: {
|
||||||
|
"#{object_name.downcase}.#{field_name}": {
|
||||||
|
operator: 'remove',
|
||||||
|
remove: 'true'
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does perform' do
|
||||||
|
before_it.call
|
||||||
|
expect(page).to have_selector(".form-group[data-attribute-name='#{field_name}'].is-removed", visible: :hidden, wait: 10)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'action - set_optional' do
|
||||||
|
before do
|
||||||
|
create(:core_workflow,
|
||||||
|
object: object_name,
|
||||||
|
perform: {
|
||||||
|
"#{object_name.downcase}.#{field_name}": {
|
||||||
|
operator: 'set_optional',
|
||||||
|
set_optional: 'true'
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does perform' do
|
||||||
|
before_it.call
|
||||||
|
expect(page.find("div[data-attribute-name='#{field_name}'] div.formGroup-label label")).to have_no_text('*', wait: 10)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'action - set_mandatory' do
|
||||||
|
before do
|
||||||
|
create(:core_workflow,
|
||||||
|
object: object_name,
|
||||||
|
perform: {
|
||||||
|
"#{object_name.downcase}.#{field_name}": {
|
||||||
|
operator: 'set_mandatory',
|
||||||
|
set_mandatory: 'true'
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does perform' do
|
||||||
|
before_it.call
|
||||||
|
expect(page.find("div[data-attribute-name='#{field_name}'] div.formGroup-label label")).to have_text('*', wait: 10)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'action - unset_readonly' do
|
||||||
|
before do
|
||||||
|
create(:core_workflow,
|
||||||
|
object: object_name,
|
||||||
|
perform: {
|
||||||
|
"#{object_name.downcase}.#{field_name}": {
|
||||||
|
operator: 'unset_readonly',
|
||||||
|
unset_readonly: 'true'
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does perform' do
|
||||||
|
before_it.call
|
||||||
|
expect(page).to have_no_selector("div[data-attribute-name='#{field_name}'].is-readonly", wait: 10)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'action - set_readonly' do
|
||||||
|
before do
|
||||||
|
create(:core_workflow,
|
||||||
|
object: object_name,
|
||||||
|
perform: {
|
||||||
|
"#{object_name.downcase}.#{field_name}": {
|
||||||
|
operator: 'set_readonly',
|
||||||
|
set_readonly: 'true'
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does perform' do
|
||||||
|
before_it.call
|
||||||
|
expect(page).to have_selector("div[data-attribute-name='#{field_name}'].is-readonly", wait: 10)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'action - restrict values' do
|
||||||
|
before do
|
||||||
|
create(:core_workflow,
|
||||||
|
object: object_name,
|
||||||
|
perform: {
|
||||||
|
"#{object_name.downcase}.#{field_name}": {
|
||||||
|
operator: 'set_fixed_to',
|
||||||
|
set_fixed_to: %w[key_1 key_3]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does perform' do
|
||||||
|
before_it.call
|
||||||
|
expect(page).to have_selector("select[name='#{field_name}'] option[value='key_1']", wait: 10)
|
||||||
|
expect(page).to have_no_selector("select[name='#{field_name}'] option[value='key_2']", wait: 10)
|
||||||
|
expect(page).to have_selector("select[name='#{field_name}'] option[value='key_3']", wait: 10)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'action - select' do
|
||||||
|
before do
|
||||||
|
create(:core_workflow,
|
||||||
|
object: object_name,
|
||||||
|
perform: {
|
||||||
|
"#{object_name.downcase}.#{field_name}": {
|
||||||
|
operator: 'select',
|
||||||
|
select: ['key_3']
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does perform' do
|
||||||
|
before_it.call
|
||||||
|
wait(5).until { page.find("select[name='#{field_name}']").value == ['key_3'] }
|
||||||
|
expect(page.find("select[name='#{field_name}']").value).to eq(['key_3'])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'action - auto select' do
|
||||||
|
before do
|
||||||
|
create(:core_workflow,
|
||||||
|
object: object_name,
|
||||||
|
perform: {
|
||||||
|
"#{object_name.downcase}.#{field_name}": {
|
||||||
|
operator: 'set_fixed_to',
|
||||||
|
set_fixed_to: ['', 'key_3'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
create(:core_workflow,
|
||||||
|
object: object_name,
|
||||||
|
perform: {
|
||||||
|
"#{object_name.downcase}.#{field_name}": {
|
||||||
|
operator: 'auto_select',
|
||||||
|
auto_select: 'true',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does perform' do
|
||||||
|
before_it.call
|
||||||
|
wait(5).until { page.find("select[name='#{field_name}']").value == ['key_3'] }
|
||||||
|
expect(page.find("select[name='#{field_name}']").value).to eq(['key_3'])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'modify boolean attribute', authenticated_as: :authenticate, db_strategy: :reset do
|
describe 'modify boolean attribute', authenticated_as: :authenticate, db_strategy: :reset do
|
||||||
def authenticate
|
def authenticate
|
||||||
create(:object_manager_attribute_boolean, object_name: object_name, name: field_name, display: field_name, screens: screens)
|
create(:object_manager_attribute_boolean, object_name: object_name, name: field_name, display: field_name, screens: screens)
|
||||||
|
|
|
@ -38,6 +38,29 @@ RSpec.describe 'Manage > Trigger', type: :system do
|
||||||
expect(find('.js-value select')).to be_multiple
|
expect(find('.js-value select')).to be_multiple
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'enables selection of multiple values for multiselect attribute' do
|
||||||
|
attribute = create_attribute :object_manager_attribute_multiselect,
|
||||||
|
data_option: {
|
||||||
|
options: {
|
||||||
|
'name 1': 'name 1',
|
||||||
|
'name 2': 'name 2',
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
null: false,
|
||||||
|
relation: '',
|
||||||
|
maxlength: 255,
|
||||||
|
nulloption: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
open_new_trigger_dialog
|
||||||
|
|
||||||
|
within '.modal .ticket_selector' do
|
||||||
|
find('.js-attributeSelector select').select(attribute.display)
|
||||||
|
|
||||||
|
expect(find('.js-value select')).to be_multiple
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sets a customer email address with no @ character' do
|
it 'sets a customer email address with no @ character' do
|
||||||
|
@ -100,4 +123,158 @@ RSpec.describe 'Manage > Trigger', type: :system do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when ticket is updated with a multiselect trigger condition', authenticated_as: :owner, db_strategy: :reset do
|
||||||
|
let(:options) do
|
||||||
|
{
|
||||||
|
a: 'a',
|
||||||
|
b: 'b',
|
||||||
|
c: 'c',
|
||||||
|
d: 'd',
|
||||||
|
e: 'e',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:trigger_values) { %w[a b c] }
|
||||||
|
|
||||||
|
let!(:attribute) do
|
||||||
|
create_attribute :object_manager_attribute_multiselect,
|
||||||
|
data_option: {
|
||||||
|
options: options,
|
||||||
|
default: '',
|
||||||
|
null: false,
|
||||||
|
relation: '',
|
||||||
|
maxlength: 255,
|
||||||
|
nulloption: true,
|
||||||
|
},
|
||||||
|
name: 'multiselect',
|
||||||
|
screens: attributes_for(:required_screen)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:group) { create(:group) }
|
||||||
|
let(:owner) { create(:admin, group_ids: [group.id]) }
|
||||||
|
let!(:ticket) { create(:ticket, group: group,) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
visit '/#manage/trigger'
|
||||||
|
click_on 'New Trigger'
|
||||||
|
|
||||||
|
modal_ready
|
||||||
|
|
||||||
|
within '.modal' do
|
||||||
|
fill_in 'Name', with: 'Test Trigger'
|
||||||
|
within '.ticket_selector' do
|
||||||
|
find('.js-attributeSelector select').select attribute.display
|
||||||
|
find('.js-operator select').select operator
|
||||||
|
trigger_values.each { |value| find('.js-value select').select value }
|
||||||
|
end
|
||||||
|
|
||||||
|
within '.ticket_perform_action' do
|
||||||
|
find('.js-attributeSelector select').select 'Note'
|
||||||
|
|
||||||
|
within '.js-setArticle' do
|
||||||
|
fill_in 'Subject', with: 'Test subject note'
|
||||||
|
find('[data-name="perform::article.note::body"]').set 'Test body note'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
click_button
|
||||||
|
end
|
||||||
|
|
||||||
|
visit "#ticket/zoom/#{ticket.id}"
|
||||||
|
|
||||||
|
ticket_multiselect_values.each do |value|
|
||||||
|
within '.sidebar-content .multiselect select' do
|
||||||
|
select value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
click_button 'Update'
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'updating the ticket with the trigger condition' do
|
||||||
|
it 'updates the ticket with the trigger condition' do
|
||||||
|
wait.until { ticket.multiselect_previously_changed? && ticket.articles.present? }
|
||||||
|
expect(ticket.articles).not_to be_empty
|
||||||
|
expect(page).to have_text 'Test body note', wait: 5
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with 'contains all' used" do
|
||||||
|
let(:operator) { 'contains all' }
|
||||||
|
|
||||||
|
context 'when updated value is the same with trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { trigger_values }
|
||||||
|
|
||||||
|
it_behaves_like 'updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when all value is selected' do
|
||||||
|
let(:ticket_multiselect_values) { options.values }
|
||||||
|
|
||||||
|
it_behaves_like 'updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with 'contains one' used" do
|
||||||
|
let(:operator) { 'contains one' }
|
||||||
|
|
||||||
|
context 'when updated value is the same with trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { trigger_values }
|
||||||
|
|
||||||
|
it_behaves_like 'updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when all value is selected' do
|
||||||
|
let(:ticket_multiselect_values) { options.values }
|
||||||
|
|
||||||
|
it_behaves_like 'updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when updated value contains only one of the trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { [trigger_values.first] }
|
||||||
|
|
||||||
|
it_behaves_like 'updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when updated value does not contain one of the trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { options.values - [trigger_values.first] }
|
||||||
|
|
||||||
|
it_behaves_like 'updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with 'contains all not' used" do
|
||||||
|
let(:operator) { 'contains all not' }
|
||||||
|
|
||||||
|
context 'when updated value is different from the trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { options.values - trigger_values }
|
||||||
|
|
||||||
|
it_behaves_like 'updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when updated value contains only one of the trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { [trigger_values.first] }
|
||||||
|
|
||||||
|
it_behaves_like 'updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when updated value does not contain one of the trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { options.values - [trigger_values.first] }
|
||||||
|
|
||||||
|
it_behaves_like 'updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "with 'contains one not' used" do
|
||||||
|
let(:operator) { 'contains one not' }
|
||||||
|
|
||||||
|
context 'when updated value is different from the trigger value' do
|
||||||
|
let(:ticket_multiselect_values) { options.values - trigger_values }
|
||||||
|
|
||||||
|
it_behaves_like 'updating the ticket with the trigger condition'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -107,6 +107,10 @@ RSpec.describe 'System > Objects', type: :system do
|
||||||
['Text', 'Select', 'Integer', 'Datetime', 'Date', 'Boolean', 'Tree Select'].each do |data_type|
|
['Text', 'Select', 'Integer', 'Datetime', 'Date', 'Boolean', 'Tree Select'].each do |data_type|
|
||||||
include_examples 'create and remove field with migration', data_type
|
include_examples 'create and remove field with migration', data_type
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with Multiselect' do
|
||||||
|
include_examples 'create and remove field with migration', 'Multiselect'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when creating and modifying tree select fields', db_strategy: :reset do
|
context 'when creating and modifying tree select fields', db_strategy: :reset do
|
||||||
|
@ -168,25 +172,113 @@ RSpec.describe 'System > Objects', type: :system do
|
||||||
# lexicographically ordered list of option strings
|
# lexicographically ordered list of option strings
|
||||||
let(:options) { %w[0 000.000 1 100.100 100.200 2 200.100 200.200 3 ä b n ö p sr ß st t ü v] }
|
let(:options) { %w[0 000.000 1 100.100 100.200 2 200.100 200.200 3 ä b n ö p sr ß st t ü v] }
|
||||||
let(:options_hash) { options.reverse.to_h { |o| [o, o] } }
|
let(:options_hash) { options.reverse.to_h { |o| [o, o] } }
|
||||||
|
let(:cutomsort_options) { ['0', '1', '2', '3', 'v', 'ü', 't', 'st', 'ß', 'sr', 'p', 'ö', 'n', 'b', 'ä', '200.200', '200.100', '100.200', '100.100', '000.000'] }
|
||||||
|
|
||||||
let(:object_attribute) do
|
before do
|
||||||
attribute = create(:object_manager_attribute_select, data_option: { options: options_hash, default: 0 }, position: 999)
|
object_attribute
|
||||||
ObjectManager::Attribute.migration_execute
|
ObjectManager::Attribute.migration_execute
|
||||||
attribute
|
|
||||||
|
refresh
|
||||||
|
|
||||||
|
visit '/#system/object_manager'
|
||||||
|
click 'tbody tr:last-child td:first-child'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'preserves the sorting correctly' do
|
shared_examples 'sorting options correctly' do
|
||||||
object_attribute
|
shared_examples 'preserving the sorting correctly' do
|
||||||
page.refresh
|
it 'preserves the sorting correctly' do
|
||||||
visit '/#system/object_manager'
|
sorted_dialog_values = all('table.settings-list tbody tr td input.js-key').map(&:value).reject { |x| x == '' }
|
||||||
click 'tbody tr:last-child'
|
expect(sorted_dialog_values).to eq(expected_options)
|
||||||
|
|
||||||
sorted_dialog_values = all('table.settings-list tbody tr td:first-child input').map(&:value).reject { |x| x == '' }
|
visit '/#ticket/create'
|
||||||
expect(sorted_dialog_values).to eq(options)
|
sorted_ticket_values = all("select[name=#{object_attribute.name}] option").map(&:value).reject { |x| x == '' }
|
||||||
|
expect(sorted_ticket_values).to eq(expected_options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
visit '/#ticket/create'
|
context 'with no customsort' do
|
||||||
sorted_ticket_values = all("select[name=#{object_attribute.name}] option").map(&:value).reject { |x| x == '' }
|
let(:data_option) { { options: options_hash, default: 0 } }
|
||||||
expect(sorted_ticket_values).to eq(options)
|
let(:expected_options) { options } # sort lexicographically
|
||||||
|
|
||||||
|
it_behaves_like 'preserving the sorting correctly'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with customsort' do
|
||||||
|
let(:options_hash) { options.reverse.collect { |o| { name: o, value: o } } }
|
||||||
|
let(:data_option) { { options: options_hash, default: 0, customsort: 'on' } }
|
||||||
|
let(:expected_options) { options.reverse } # preserves sorting from backend
|
||||||
|
|
||||||
|
it_behaves_like 'preserving the sorting correctly'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'sorting options correctly using drag and drop' do
|
||||||
|
shared_examples 'preserving drag and drop sorting correctly' do
|
||||||
|
it 'preserves drag and drop sorting correctly' do
|
||||||
|
sorted_dialog_values = all('table.settings-list tbody tr td input.js-key').map(&:value).reject { |x| x == '' }
|
||||||
|
expect(sorted_dialog_values).to eq(expected_options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with drag and drop sorting' do
|
||||||
|
let(:options) { %w[0 1 d u w] }
|
||||||
|
let(:options_hash) { options.to_h { |o| [o, o] } }
|
||||||
|
|
||||||
|
before do
|
||||||
|
# use drag and drop to reverse sort the options
|
||||||
|
within '.modal form' do
|
||||||
|
within '.js-dataMap table.js-Table .table-sortable' do
|
||||||
|
rows = all('tr.input-data-row td.table-draggable')
|
||||||
|
target = rows.last
|
||||||
|
pos = rows.size - 1
|
||||||
|
rows.each do |row|
|
||||||
|
next if pos <= 0
|
||||||
|
|
||||||
|
row.drag_to target
|
||||||
|
pos -= 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
click_button 'Submit'
|
||||||
|
end
|
||||||
|
|
||||||
|
click '.js-execute', wait: 7.minutes
|
||||||
|
# expect(page).to have_text('please reload your browser')
|
||||||
|
click '.modal-content button.js-submit'
|
||||||
|
|
||||||
|
refresh
|
||||||
|
|
||||||
|
visit '/#system/object_manager'
|
||||||
|
click 'tbody tr:last-child td:first-child'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with no customsort' do
|
||||||
|
let(:data_option) { { options: options_hash, default: 0 } }
|
||||||
|
let(:expected_options) { options } # sort lexicographically
|
||||||
|
|
||||||
|
it_behaves_like 'preserving drag and drop sorting correctly'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with customsort' do
|
||||||
|
let(:data_option) { { options: options_hash, default: 0, customsort: 'on' } }
|
||||||
|
let(:expected_options) { options.reverse } # preserves sorting from backend
|
||||||
|
|
||||||
|
it_behaves_like 'preserving drag and drop sorting correctly'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with multiselect attribute' do
|
||||||
|
let(:object_attribute) { create(:object_manager_attribute_multiselect, data_option: data_option, position: 999) }
|
||||||
|
|
||||||
|
it_behaves_like 'sorting options correctly'
|
||||||
|
it_behaves_like 'sorting options correctly using drag and drop'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with select attribute' do
|
||||||
|
let(:object_attribute) { create(:object_manager_attribute_select, data_option: data_option, position: 999) }
|
||||||
|
|
||||||
|
it_behaves_like 'sorting options correctly'
|
||||||
|
it_behaves_like 'sorting options correctly using drag and drop'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -364,6 +456,34 @@ RSpec.describe 'System > Objects', type: :system do
|
||||||
expect(ObjectManager::Attribute.last.data_option['options']).to eq(expected_data_options)
|
expect(ObjectManager::Attribute.last.data_option['options']).to eq(expected_data_options)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'checks smart defaults for multiselect field' do
|
||||||
|
fill_in 'Name', with: 'multiselect1'
|
||||||
|
find('input[name=display]').set('multiselect1')
|
||||||
|
|
||||||
|
page.find('select[name=data_type]').select('Multiselect')
|
||||||
|
|
||||||
|
page.first('div.js-add').click
|
||||||
|
page.first('div.js-add').click
|
||||||
|
page.first('div.js-add').click
|
||||||
|
|
||||||
|
counter = 0
|
||||||
|
page.all('.js-key').each do |field|
|
||||||
|
field.set(counter)
|
||||||
|
counter += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
page.all('.js-value')[-2].set('special 2')
|
||||||
|
page.find('.js-submit').click
|
||||||
|
|
||||||
|
expected_data_options = {
|
||||||
|
'0' => '0',
|
||||||
|
'1' => '1',
|
||||||
|
'2' => 'special 2',
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(ObjectManager::Attribute.last.data_option['options']).to eq(expected_data_options)
|
||||||
|
end
|
||||||
|
|
||||||
it 'checks smart defaults for boolean field' do
|
it 'checks smart defaults for boolean field' do
|
||||||
fill_in 'Name', with: 'bool1'
|
fill_in 'Name', with: 'bool1'
|
||||||
find('input[name=display]').set('bool1')
|
find('input[name=display]').set('bool1')
|
||||||
|
@ -528,4 +648,99 @@ RSpec.describe 'System > Objects', type: :system do
|
||||||
expect { page.find('.js-submit').click }.to change(ObjectManager::Attribute, :count).by(1)
|
expect { page.find('.js-submit').click }.to change(ObjectManager::Attribute, :count).by(1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with drag and drop custom sort', db_strategy: :reset do
|
||||||
|
before do
|
||||||
|
visit '/#system/object_manager'
|
||||||
|
page.find('.js-new').click
|
||||||
|
|
||||||
|
page.find('select[name=data_type]').select data_type
|
||||||
|
fill_in 'Name', with: attribute_name
|
||||||
|
find('input[name=display]').set attribute_name
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:attribute) { ObjectManager::Attribute.find_by(name: attribute_name) }
|
||||||
|
let(:data_options) do
|
||||||
|
{
|
||||||
|
'1' => 'one',
|
||||||
|
'2' => 'two',
|
||||||
|
'3' => 'three',
|
||||||
|
'4' => 'four',
|
||||||
|
'5' => 'five'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'having a custom sort option' do
|
||||||
|
it 'has a custom option checkbox' do
|
||||||
|
within '.modal-dialog form' do
|
||||||
|
expect(page).to have_field('data_option::customsort', type: 'checkbox', visible: :all)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'a context' do
|
||||||
|
before do
|
||||||
|
within '.modal-dialog form' do
|
||||||
|
within 'tr.input-add-row' do
|
||||||
|
5.times.each { first('div.js-add').click }
|
||||||
|
end
|
||||||
|
|
||||||
|
keys = data_options.keys
|
||||||
|
all_value_input = all('tr.input-data-row .js-value')
|
||||||
|
all_key_input = all('tr.input-data-row .js-key')
|
||||||
|
|
||||||
|
keys.each_with_index do |key, index|
|
||||||
|
all_key_input[index].set key
|
||||||
|
all_value_input[index].set data_options[key]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with custom checkbox checked' do
|
||||||
|
it 'saves a customsort data option attribute' do
|
||||||
|
within '.modal-dialog form' do
|
||||||
|
check 'data_option::customsort', allow_label_click: true
|
||||||
|
click_button
|
||||||
|
end
|
||||||
|
|
||||||
|
# Update Database
|
||||||
|
click 'div.js-execute'
|
||||||
|
# Reload browser
|
||||||
|
refresh
|
||||||
|
|
||||||
|
expect(attribute['data_option']).to include('customsort' => 'on')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with custom checkbox unchecked' do
|
||||||
|
it 'does not have a customsort data option attribute' do
|
||||||
|
within '.modal-dialog form' do
|
||||||
|
uncheck 'data_option::customsort', allow_label_click: true
|
||||||
|
click_button
|
||||||
|
end
|
||||||
|
|
||||||
|
# Update Database
|
||||||
|
click 'div.js-execute'
|
||||||
|
# Reload browser
|
||||||
|
refresh
|
||||||
|
|
||||||
|
expect(attribute['data_option']).not_to include('customsort' => 'on')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when attribute is multiselect' do
|
||||||
|
let(:data_type) { 'Multiselect' }
|
||||||
|
let(:attribute_name) { 'multiselect_test' }
|
||||||
|
|
||||||
|
it_behaves_like 'having a custom sort option'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when attribute is select' do
|
||||||
|
let(:data_type) { 'Select' }
|
||||||
|
let(:attribute_name) { 'select_test' }
|
||||||
|
|
||||||
|
it_behaves_like 'having a custom sort option'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2376,4 +2376,68 @@ RSpec.describe 'Ticket zoom', type: :system do
|
||||||
expect(page).to have_select('state_id', selected: 'new')
|
expect(page).to have_select('state_id', selected: 'new')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'Multiselect displaying and saving', authenticated_as: :authenticate, db_strategy: :reset do
|
||||||
|
let(:field_name) { SecureRandom.uuid }
|
||||||
|
let(:ticket) { create(:ticket, group: Group.find_by(name: 'Users'), field_name => %w[key_2 key_3]) }
|
||||||
|
|
||||||
|
def authenticate
|
||||||
|
create :object_manager_attribute_multiselect, name: field_name, display: field_name, screens: {
|
||||||
|
'edit' => {
|
||||||
|
'ticket.agent' => {
|
||||||
|
'shown' => true,
|
||||||
|
'required' => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ObjectManager::Attribute.migration_execute
|
||||||
|
ticket
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
visit "#ticket/zoom/#{ticket.id}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def multiselect_value
|
||||||
|
page.find("select[name='#{field_name}']").value
|
||||||
|
end
|
||||||
|
|
||||||
|
def multiselect_set(values)
|
||||||
|
multiselect_unset_all
|
||||||
|
values = Array(values)
|
||||||
|
values.each do |value|
|
||||||
|
page.find("select[name='#{field_name}']").select(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def multiselect_unset_all
|
||||||
|
values = page.all("select[name='#{field_name}'] option").map(&:text)
|
||||||
|
values.each do |value|
|
||||||
|
page.find("select[name='#{field_name}']").unselect(value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does show values properly and can save values also' do
|
||||||
|
|
||||||
|
# check ticket state rendering
|
||||||
|
wait(5).until { multiselect_value == %w[key_2 key_3] }
|
||||||
|
expect(multiselect_value).to eq(%w[key_2 key_3])
|
||||||
|
|
||||||
|
# save 2 values
|
||||||
|
multiselect_set(%w[value_1 value_2])
|
||||||
|
click '.js-submit'
|
||||||
|
expect(ticket.reload[field_name]).to eq(%w[key_1 key_2])
|
||||||
|
|
||||||
|
# save 1 value
|
||||||
|
multiselect_set(['value_1'])
|
||||||
|
click '.js-submit'
|
||||||
|
expect(ticket.reload[field_name]).to eq(['key_1'])
|
||||||
|
|
||||||
|
# unset all values
|
||||||
|
multiselect_unset_all
|
||||||
|
click '.js-submit'
|
||||||
|
expect(ticket.reload[field_name]).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue