Fixes #3917 - Multiselect field support.
This commit is contained in:
parent
db0eb45fe1
commit
93932055dd
36 changed files with 1739 additions and 204 deletions
|
@ -530,6 +530,9 @@ class App.ControllerForm extends App.Controller
|
|||
param[item.name] = [param[item.name], value]
|
||||
else
|
||||
param[item.name].push value
|
||||
else
|
||||
if item.multiselect && typeof value is 'string'
|
||||
param[item.name] = new Array(value)
|
||||
else
|
||||
param[item.name] = value
|
||||
|
||||
|
|
|
@ -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')]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
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]) )
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -124,14 +124,20 @@ class App.FormHandlerCoreWorkflow
|
|||
coreWorkflowRestrictions[classname][item.name] = App.FormHandlerCoreWorkflow.restrictValuesAttributeCache(attribute, values)
|
||||
|
||||
valueFound = false
|
||||
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())
|
||||
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
|
||||
|
||||
|
|
|
@ -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%;">
|
||||
<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>
|
|
@ -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,
|
||||
|
@ -251,24 +252,26 @@ jQuery.fn.extend( {
|
|||
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();
|
||||
|
|
|
@ -3680,6 +3680,7 @@ ol.tabs li {
|
|||
|
||||
.table-draggable & {
|
||||
vertical-align: middle;
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -39,8 +39,12 @@ module ChecksCoreWorkflow
|
|||
end
|
||||
|
||||
def restricted_value?(perform_result, key)
|
||||
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)
|
||||
perform_result[:mandatory].each_key do |key|
|
||||
|
|
|
@ -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?('')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,7 +778,8 @@ condition example
|
|||
query += "#{attribute} NOT #{like} (?)"
|
||||
value = "%#{selector['value']}%"
|
||||
bind_params.push value
|
||||
elsif selector['operator'] == 'contains all' && attributes[0] == 'ticket' && attributes[1] == 'tags'
|
||||
elsif selector['operator'] == 'contains all'
|
||||
if attributes[0] == 'ticket' && attributes[1] == 'tags'
|
||||
query += "? = (
|
||||
SELECT
|
||||
COUNT(*)
|
||||
|
@ -796,7 +796,11 @@ condition example
|
|||
)"
|
||||
bind_params.push selector['value'].count
|
||||
bind_params.push selector['value']
|
||||
elsif selector['operator'] == 'contains one' && attributes[0] == 'ticket' && attributes[1] == 'tags'
|
||||
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
|
||||
|
@ -806,7 +810,11 @@ condition example
|
|||
tag_items.name IN (?)"
|
||||
|
||||
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 += 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(*)
|
||||
|
@ -822,7 +830,11 @@ condition example
|
|||
tag_items.name IN (?)
|
||||
)"
|
||||
bind_params.push selector['value']
|
||||
elsif selector['operator'] == 'contains one not' && attributes[0] == 'ticket' && attributes[1] == 'tags'
|
||||
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(*)
|
||||
|
@ -838,6 +850,9 @@ condition example
|
|||
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']
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
|
|
@ -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'
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 { '' }
|
||||
|
||||
|
|
|
@ -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
|
||||
let(:renderer) do
|
||||
build :notification_factory_renderer,
|
||||
objects: { ticket: ticket },
|
||||
template: template
|
||||
end
|
||||
|
||||
shared_examples 'correctly rendering the attributes' do
|
||||
it 'correctly renders the attributes' do
|
||||
expect(renderer.render).to eq expected_render
|
||||
end
|
||||
end
|
||||
|
||||
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' }
|
||||
|
||||
it_behaves_like 'correctly rendering the attributes'
|
||||
end
|
||||
|
||||
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'
|
||||
ObjectManager::Attribute.migration_execute
|
||||
end
|
||||
|
||||
let(:user) do
|
||||
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'
|
||||
user
|
||||
end
|
||||
|
||||
it 'correctly renders select attributes on chained group object' do
|
||||
let(:ticket) { create :ticket, customer: user }
|
||||
let(:template) { '#{ticket.customer.select} _SEPERATOR_ #{ticket.customer.select.value}' }
|
||||
let(:expected_render) { 'key_2 _SEPERATOR_ value_2' }
|
||||
|
||||
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'
|
||||
ObjectManager::Attribute.migration_execute
|
||||
end
|
||||
let(:template) { '#{ticket.group.select} _SEPERATOR_ #{ticket.group.select.value}' }
|
||||
let(:expected_render) { 'key_3 _SEPERATOR_ value_3' }
|
||||
|
||||
ticket = create :ticket, customer: @user
|
||||
let(:ticket) { create :ticket, customer: @user }
|
||||
|
||||
before do
|
||||
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
|
||||
|
||||
it 'correctly renders select attributes on chained organization object' do
|
||||
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'
|
||||
ObjectManager::Attribute.migration_execute
|
||||
|
||||
@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'
|
||||
end
|
||||
|
||||
it 'correctly renders tree select attributes' do
|
||||
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'
|
||||
ObjectManager::Attribute.migration_execute
|
||||
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' }
|
||||
|
||||
ticket = create :ticket, customer: @user, tree_select: 'Incident::Hardware::Laptop'
|
||||
|
||||
renderer = build :notification_factory_renderer,
|
||||
objects: { ticket: ticket },
|
||||
template: '#{ticket.tree_select} _SEPERATOR_ #{ticket.tree_select.value}'
|
||||
|
||||
expect(renderer.render).to eq 'Incident::Hardware::Laptop _SEPERATOR_ Incident::Hardware::Laptop'
|
||||
it_behaves_like 'correctly rendering the attributes'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
shared_examples 'sorting options correctly' do
|
||||
shared_examples 'preserving the sorting correctly' do
|
||||
it 'preserves the sorting correctly' do
|
||||
object_attribute
|
||||
page.refresh
|
||||
visit '/#system/object_manager'
|
||||
click 'tbody tr:last-child'
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
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)
|
||||
expect(sorted_ticket_values).to eq(expected_options)
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue