Fixes #3917 - Multiselect field support.

This commit is contained in:
Bola Ahmed Buari 2022-01-20 11:07:12 +01:00 committed by Rolf Schmidt
parent db0eb45fe1
commit 93932055dd
36 changed files with 1739 additions and 204 deletions

View file

@ -531,7 +531,10 @@ class App.ControllerForm extends App.Controller
else
param[item.name].push value
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
uncheckParam = {}

View file

@ -29,6 +29,7 @@ class App.UiElement.ApplicationSelector
'integer$': [__('is'), __('is not')]
'^radio$': [__('is'), __('is not')]
'^select$': [__('is'), __('is not')]
'^multiselect$': [__('contains all'), __('contains one'), __('contains all not'), __('contains one not')]
'^tree_select$': [__('is'), __('is not')]
'^input$': [__('contains'), __('contains not')]
'^richtext$': [__('contains'), __('contains not')]
@ -44,6 +45,7 @@ class App.UiElement.ApplicationSelector
'integer$': [__('is'), __('is not'), __('has changed')]
'^radio$': [__('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')]
'^input$': [__('contains'), __('contains not'), __('has changed')]
'^richtext$': [__('contains'), __('contains not'), __('has changed')]

View file

@ -72,6 +72,27 @@ class App.UiElement.ApplicationUiElement
}
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) ->
# build options list based on relation

View file

@ -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')]
'integer$': [__('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')]
'^(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
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)
# ignore passwords and relations
@ -155,7 +156,7 @@ class App.UiElement.core_workflow_condition extends App.UiElement.ApplicationSel
config = _.clone(row)
if config.tag is 'textarea'
config.expanding = false
if config.tag is 'select'
if /^((multi)?select)$/.test(config.tag)
config.multiple = true
config.default = undefined
if config.type is 'email' || config.type is 'tel'

View file

@ -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']
'integer$': ['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']
'^(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
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 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)
if config.tag is 'boolean'
config.tag = 'select'
if config.tag is 'select'
if /^((multi)?select)$/.test(config.tag)
config.multiple = true
config.default = undefined
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')
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" />')
return
super(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
@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.nulloption = true
else

View file

@ -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

View file

@ -7,14 +7,8 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
if params.data_option_new && !_.isEmpty(params.data_option_new)
params.data_option = params.data_option_new
if attribute.value == 'select' && params.data_option? && params.data_option.options?
sorted = _.map(
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]) )
if /^((multi)?select)$/.test(attribute.value) && params.data_option? && params.data_option.options?
params.data_option.mapped = @mapDataOptions(params.data_option)
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.find('.js-dataMap').html(element)
localItem.find('.js-dataScreens').html(@dataScreens(attribute, localParams, params))
@addDragAndDrop(localItem)
options =
datetime: __('Datetime')
@ -38,6 +33,7 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
tree_select: __('Tree Select')
boolean: __('Boolean')
integer: __('Integer')
multiselect: __('Multiselect')
# if attribute already exists, do not allow to change it anymore
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)
@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) ->
newRow = element.find('.js-template').clone().removeClass('js-template')
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-autocompletionUrl').html(autocompletionUrl.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]) )

View file

@ -15,7 +15,7 @@ class App.UiElement.select extends App.UiElement.ApplicationUiElement
@addDeletedOptions(attribute, params)
# build options list based on config
@getConfigOptionList(attribute, params)
@getConfigCustomSortOptionList(attribute)
# build options list based on relation
@getRelationOptionList(attribute, params)

View file

@ -39,9 +39,16 @@ treeParams = (e, params) ->
params.data_option.options = tree
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) ->
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(->
element = $(@)
@ -54,6 +61,19 @@ setSelectDefaults = (el) ->
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
requiredPermission: 'admin.object'
constructor: ->
@ -198,6 +218,8 @@ class New extends App.ControllerGenericNew
params = @formParam(e.target)
params = treeParams(e, params)
params = multiselectParams(params)
params = customsortDataOptions(e, params)
# show attributes for create_middle in two column style
if params.screens && params.screens.create_middle
@ -261,6 +283,8 @@ class Edit extends App.ControllerGenericEdit
params = @formParam(e.target)
params = treeParams(e, params)
params = multiselectParams(params)
params = customsortDataOptions(e, params)
# show attributes for create_middle in two column style
if params.screens && params.screens.create_middle

View file

@ -124,16 +124,22 @@ class App.FormHandlerCoreWorkflow
coreWorkflowRestrictions[classname][item.name] = App.FormHandlerCoreWorkflow.restrictValuesAttributeCache(attribute, values)
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)
if value isnt undefined && paramValue isnt undefined && value isnt null && paramValue isnt null
if value.toString() == paramValue.toString()
valueFound = true
break
if _.isArray(paramValue) && _.contains(paramValue, value.toString())
valueFound = true
break
# false values are valid values e.g. for boolean fields (be careful)
continue if value is undefined
continue if value is null
continue if paramValue is undefined
continue if paramValue is null
continue if value.toString() != paramValue.toString()
valueFound = true
break
item.filter = values
if valueFound

View file

@ -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>

View file

@ -2,15 +2,17 @@
<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>
<% if @params.data_option && @params.data_option.sorted: %>
<% for [key, display] in @params.data_option.sorted: %>
<tr>
<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 class="settings-list-control-cell">
@ -23,7 +25,8 @@
</div>
<% end %>
<% end %>
<tr>
<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">
@ -38,7 +41,8 @@
</table>
<table class="hidden">
<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">
<input class="form-control form-control--small js-key" type="text" value="" required/>
<td class="settings-list-control-cell">
@ -50,5 +54,14 @@
<%- @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>
</div>

View file

@ -225,6 +225,7 @@ jQuery.fn.removeAttrs = function(regex) {
// changes
// - set type based on data('field-type')
// - also catch [disabled] params
// - return multiselect type to make sure that the data is always array
jQuery.fn.extend( {
serializeArrayWithType: function() {
var r20 = /%20/g,
@ -248,27 +249,29 @@ jQuery.fn.extend( {
( this.checked || !rcheckableType.test( type ) );
} )
.map( function( i, elem ) {
var $elem = jQuery( this );
var val = $elem.val();
var type = $elem.data('field-type');
var $elem = jQuery( this );
var val = $elem.val();
var type = $elem.data('field-type');
var multiple = $elem.prop('multiple');
var multiselect = multiple && $elem.hasClass('multiselect');
var result;
if ( val == null ) {
// be sure that also null values are transferred
// https://github.com/zammad/zammad/issues/944
if ($elem.prop('multiple')) {
result = { name: elem.name, value: null, type: type }
result = { name: elem.name, value: null, type: type, multiselect: multiselect }
} else {
result = null
}
}
else if ( jQuery.isArray( 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 {
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;
} ).get();

View file

@ -3680,6 +3680,7 @@ ol.tabs li {
.table-draggable & {
vertical-align: middle;
cursor: move;
}
}

View file

@ -98,7 +98,7 @@ class ObjectManagerAttributesController < ApplicationController
if permitted[:data_option]
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

View file

@ -39,7 +39,11 @@ module ChecksCoreWorkflow
end
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
def check_mandatory(perform_result)

View file

@ -234,8 +234,8 @@ class CoreWorkflow::Attributes
return values if values == ['']
saved_value = saved_attribute_value(attribute)
if saved_value.present? && values.exclude?(saved_value)
values |= Array(saved_value.to_s)
if saved_value.present?
values |= Array(saved_value).map(&:to_s)
end
if attribute[:nulloption] && values.exclude?('')

View file

@ -25,19 +25,19 @@ class CoreWorkflow::Result::Backend
def saved_value
# 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
# if no changes happend to the form. If the users does changes
# 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
# or if attribute is only available in the frontend but not
# 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
def attribute

View file

@ -10,7 +10,7 @@ class CoreWorkflow::Result::RemoveOption < CoreWorkflow::Result::BaseOption
def config_value
result = Array(@perform_config['remove_option'])
result -= Array(saved_value)
result -= saved_value
result
end
end

View file

@ -13,7 +13,7 @@ class CoreWorkflow::Result::SetFixedTo < CoreWorkflow::Result::BaseOption
def config_value
result = Array(@perform_config['set_fixed_to'])
result |= Array(saved_value)
result |= saved_value
result
end

View file

@ -9,6 +9,7 @@ class ObjectManager::Attribute < ApplicationModel
user_autocompletion
checkbox
select
multiselect
tree_select
datetime
date
@ -42,6 +43,9 @@ class ObjectManager::Attribute < ApplicationModel
before_validation :set_base_options
before_create :ensure_multiselect
before_update :ensure_multiselect
scope :active, -> { where(active: true) }
scope :editable, -> { where(editable: true) }
scope :for_object, lambda { |name_or_klass|
@ -588,11 +592,17 @@ to send no browser reload event, pass false
data_type = nil
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
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
when %r{^boolean|active$}
when %r{^(boolean|active)$}
data_type = :boolean
when %r{^datetime$}
data_type = :datetime
@ -603,7 +613,7 @@ to send no browser reload event, pass false
# change field
if model.column_names.include?(attribute.name)
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(
model.table_name,
attribute.name,
@ -611,7 +621,21 @@ to send no browser reload event, pass false
limit: attribute.data_option[:maxlength],
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(
model.table_name,
attribute.name,
@ -635,7 +659,7 @@ to send no browser reload event, pass false
# create field
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(
model.table_name,
attribute.name,
@ -643,7 +667,21 @@ to send no browser reload event, pass false
limit: attribute.data_option[:maxlength],
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(
model.table_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?
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[:maxlength] ||= 255
end
@ -889,7 +927,7 @@ is certain attribute used by triggers, overviews or schedulers
end
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_change - allowable_changes).empty?
@ -961,7 +999,7 @@ is certain attribute used by triggers, overviews or schedulers
data_option_maxlength_check
when 'integer'
data_option_min_max_check
when %r{^((tree_)?select|checkbox)$}
when %r{^((multi|tree_)?select|checkbox)$}
data_option_default_check + data_option_relation_check
when 'boolean'
data_option_default_check + data_option_nil_check
@ -971,4 +1009,11 @@ is certain attribute used by triggers, overviews or schedulers
[]
end
end
def ensure_multiselect
return if data_type != 'multiselect'
return if data_option && data_option[:multiple] == true
data_option[:multiple] = true
end
end

View file

@ -684,7 +684,6 @@ condition example
end
next
end
if selector['operator'] == 'is'
if selector['pre_condition'] == 'not_set'
if attributes[1].match?(%r{^(created_by|updated_by|owner|customer|user)_id})
@ -779,65 +778,81 @@ condition example
query += "#{attribute} NOT #{like} (?)"
value = "%#{selector['value']}%"
bind_params.push value
elsif selector['operator'] == 'contains all' && attributes[0] == 'ticket' && attributes[1] == 'tags'
query += "? = (
SELECT
COUNT(*)
FROM
tag_objects,
tag_items,
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 (?)
)"
bind_params.push selector['value'].count
bind_params.push selector['value']
elsif selector['operator'] == 'contains one' && attributes[0] == 'ticket' && attributes[1] == 'tags'
tables += ', tag_objects, tag_items, tags'
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 (?)"
elsif selector['operator'] == 'contains all'
if attributes[0] == 'ticket' && attributes[1] == 'tags'
query += "? = (
SELECT
COUNT(*)
FROM
tag_objects,
tag_items,
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 (?)
)"
bind_params.push selector['value'].count
bind_params.push selector['value']
elsif Ticket.column_names.include?(attributes[1])
query += SqlHelper.new(object: Ticket).array_contains_all(attributes[1], selector['value'])
end
elsif selector['operator'] == 'contains one' && attributes[0] == 'ticket'
if attributes[1] == 'tags'
tables += ', tag_objects, tag_items, tags'
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']
elsif selector['operator'] == 'contains all not' && attributes[0] == 'ticket' && attributes[1] == 'tags'
query += "0 = (
SELECT
COUNT(*)
FROM
tag_objects,
tag_items,
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 (?)
)"
bind_params.push selector['value']
elsif selector['operator'] == 'contains one not' && attributes[0] == 'ticket' && attributes[1] == 'tags'
query += "(
SELECT
COUNT(*)
FROM
tag_objects,
tag_items,
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']
bind_params.push selector['value']
elsif Ticket.column_names.include?(attributes[1])
query += SqlHelper.new(object: Ticket).array_contains_one(attributes[1], selector['value'])
end
elsif selector['operator'] == 'contains all not' && attributes[0] == 'ticket'
if attributes[1] == 'tags'
query += "0 = (
SELECT
COUNT(*)
FROM
tag_objects,
tag_items,
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 (?)
)"
bind_params.push selector['value']
elsif Ticket.column_names.include?(attributes[1])
query += SqlHelper.new(object: Ticket).array_contains_all(attributes[1], selector['value'], negated: true)
end
elsif selector['operator'] == 'contains one not' && attributes[0] == 'ticket'
if attributes[1] == 'tags'
query += "(
SELECT
COUNT(*)
FROM
tag_objects,
tag_items,
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)'
query += "#{attribute} <= ?"
bind_params.push selector['value']

View file

@ -3,6 +3,7 @@
case ActiveRecord::Base.connection_config[:adapter]
when 'mysql2'
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_like = 'LIKE'
Rails.application.config.db_null_byte = true
@ -15,6 +16,7 @@ when 'mysql2'
end
when 'postgresql'
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_like = 'ILIKE'
Rails.application.config.db_null_byte = false

View file

@ -433,6 +433,7 @@ msgstr ""
#: 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/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/tree_select.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/sla_modal.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/profile/linked_accounts.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:"
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
msgid "Checkmk"
msgstr ""
@ -2418,6 +2425,7 @@ msgstr ""
#: app/assets/javascripts/app/views/channel/chat.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/multiselect.jst.eco
#: app/assets/javascripts/app/views/object_manager/attribute/select.jst.eco
#: db/seeds/settings.rb
msgid "Default"
@ -3220,6 +3228,7 @@ msgstr ""
#: 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/multiselect.jst.eco
#: app/assets/javascripts/app/views/object_manager/attribute/select.jst.eco
#: app/assets/javascripts/app/views/object_manager/index.jst.eco
msgid "Display"
@ -5283,6 +5292,7 @@ msgid "Keep messages on server"
msgstr ""
#: 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/tree_select.jst.eco
msgid "Key"
@ -5981,6 +5991,10 @@ msgstr ""
msgid "Moved out"
msgstr ""
#: app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee
msgid "Multiselect"
msgstr ""
#: db/seeds/overviews.rb
msgid "My Organization Tickets"
msgstr ""
@ -7498,6 +7512,7 @@ msgstr ""
#: app/assets/javascripts/app/views/integration/placetel.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/object_manager/attribute/multiselect.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/twitter/search_term.jst.eco
@ -9839,6 +9854,11 @@ msgstr ""
msgid "Use client storage to cache data to enhance performance of application."
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
msgid "Use one line per URI"
msgstr ""
@ -10830,18 +10850,22 @@ msgid "connected"
msgstr ""
#: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
#: app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee
msgid "contains"
msgstr ""
#: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
#: app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee
msgid "contains all"
msgstr ""
#: 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"
msgstr ""
#: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
#: app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee
msgid "contains not"
msgstr ""
@ -11015,6 +11039,7 @@ msgid "h"
msgstr ""
#: 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
msgid "has changed"
msgstr ""

View file

@ -199,14 +199,19 @@ examples how to use
def display_value(object, method_name, previous_method_names, key)
return key if method_name != 'value' ||
!key.instance_of?(String)
(!key.instance_of?(String) && !key.instance_of?(Array))
attributes = ObjectManager::Attribute
.where(object_lookup_id: ObjectLookup.by_name(object.class.to_s))
.where(name: previous_method_names.split('.').last)
return key if attributes.count.zero? || attributes.first.data_type != 'select'
attributes.first.data_option['options'][key] || key
case attributes.first.data_type
when 'select'
attributes.first.data_option['options'][key] || key
when 'multiselect'
key.map { |k| attributes.first.data_option['options'][k] || k }
else
key
end
end
end

View file

@ -6,6 +6,14 @@ class SqlHelper
@object = object
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)
sort_by = []
if params[key].present? && params[key].is_a?(String)
@ -97,7 +105,7 @@ order_by = [
def set_sql_order_default(sql, default)
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
sql
end
@ -128,7 +136,7 @@ sql = 'tickets.created_at, tickets.updated_at'
next if value.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
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 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
sql = set_sql_order_default(sql, default)
sql.join(', ')
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

View file

@ -32,6 +32,13 @@ RSpec.describe CheckForObjectAttributes, type: :db_migration do
expect { migrate }
.not_to change { attribute.reload.data_option }
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
context 'for #data_option key:' do

View file

@ -156,6 +156,28 @@ FactoryBot.define do
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
default { '' }

View file

@ -60,84 +60,250 @@ RSpec.describe NotificationFactory::Renderer do
end
context 'when handling ObjectManager::Attribute usage', db_strategy: :reset do
it 'correctly renders simple select attributes' do
create :object_manager_attribute_select, name: 'select'
before do
create_object_manager_attribute
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
it 'correctly renders select attributes on chained user object' do
create :object_manager_attribute_select,
object_lookup_id: ObjectLookup.by_name('User'),
name: 'select'
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'
let(:renderer) do
build :notification_factory_renderer,
objects: { ticket: ticket },
template: template
end
it 'correctly renders select attributes on chained group object' do
create :object_manager_attribute_select,
object_lookup_id: ObjectLookup.by_name('Group'),
name: 'select'
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'
shared_examples 'correctly rendering the attributes' do
it 'correctly renders the attributes' do
expect(renderer.render).to eq expected_render
end
end
it 'correctly renders select attributes on chained organization object' do
create :object_manager_attribute_select,
object_lookup_id: ObjectLookup.by_name('Organization'),
name: 'select'
ObjectManager::Attribute.migration_execute
context 'with a simple select attribute' do
let(:create_object_manager_attribute) do
create :object_manager_attribute_select, name: 'select'
end
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'
@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'
it_behaves_like 'correctly rendering the attributes'
end
it 'correctly renders tree select attributes' do
create :object_manager_attribute_tree_select, name: 'tree_select'
ObjectManager::Attribute.migration_execute
context 'with select attribute on chained user object' do
let(:create_object_manager_attribute) do
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,
objects: { ticket: ticket },
template: '#{ticket.tree_select} _SEPERATOR_ #{ticket.tree_select.value}'
let(:ticket) { create :ticket, customer: user }
let(:template) { '#{ticket.customer.select} _SEPERATOR_ #{ticket.customer.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

View file

@ -299,6 +299,37 @@ RSpec.describe CoreWorkflow, type: :model do
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
it 'does not show pending time for non pending state' do
expect(result[:visibility]['pending_time']).to eq('remove')

View file

@ -931,4 +931,220 @@ RSpec.describe Trigger, type: :model do
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

View file

@ -16,4 +16,16 @@ RSpec.configure do |config|
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

View file

@ -619,6 +619,206 @@ RSpec.shared_examples 'core workflow' do
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
def authenticate
create(:object_manager_attribute_boolean, object_name: object_name, name: field_name, display: field_name, screens: screens)

View file

@ -38,6 +38,29 @@ RSpec.describe 'Manage > Trigger', type: :system do
expect(find('.js-value select')).to be_multiple
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
it 'sets a customer email address with no @ character' do
@ -100,4 +123,158 @@ RSpec.describe 'Manage > Trigger', type: :system do
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

View file

@ -107,6 +107,10 @@ RSpec.describe 'System > Objects', type: :system do
['Text', 'Select', 'Integer', 'Datetime', 'Date', 'Boolean', 'Tree Select'].each do |data_type|
include_examples 'create and remove field with migration', data_type
end
context 'with Multiselect' do
include_examples 'create and remove field with migration', 'Multiselect'
end
end
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
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(: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
attribute = create(:object_manager_attribute_select, data_option: { options: options_hash, default: 0 }, position: 999)
before do
object_attribute
ObjectManager::Attribute.migration_execute
attribute
refresh
visit '/#system/object_manager'
click 'tbody tr:last-child td:first-child'
end
it 'preserves the sorting correctly' do
object_attribute
page.refresh
visit '/#system/object_manager'
click 'tbody tr:last-child'
shared_examples 'sorting options correctly' do
shared_examples 'preserving the sorting correctly' do
it 'preserves the 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)
sorted_dialog_values = all('table.settings-list tbody tr td:first-child input').map(&:value).reject { |x| x == '' }
expect(sorted_dialog_values).to eq(options)
visit '/#ticket/create'
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'
sorted_ticket_values = all("select[name=#{object_attribute.name}] option").map(&:value).reject { |x| x == '' }
expect(sorted_ticket_values).to eq(options)
context 'with no customsort' do
let(:data_option) { { options: options_hash, default: 0 } }
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
@ -364,6 +456,34 @@ RSpec.describe 'System > Objects', type: :system do
expect(ObjectManager::Attribute.last.data_option['options']).to eq(expected_data_options)
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
fill_in 'Name', with: '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)
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

View file

@ -2376,4 +2376,68 @@ RSpec.describe 'Ticket zoom', type: :system do
expect(page).to have_select('state_id', selected: 'new')
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