Browse Source

Fixes #4709 - Simplify the definition of multiple values for specific condition operators.

Co-authored-by: Florian Liebe <fl@zammad.com>
Co-authored-by: Dusan Vuckovic <dv@zammad.com>
Florian Liebe 1 year ago
parent
commit
1045e20b6c

+ 4 - 0
app/assets/javascripts/app/controllers/_application_controller/form.coffee

@@ -535,6 +535,10 @@ class App.ControllerForm extends App.Controller
         delete param[item.name]
         continue
 
+      # Support data value attributes that override field value.
+      if field.get(0).hasAttribute('data-value')
+        item.value = field.get(0).getAttribute('data-value')
+
       # collect all params, push it to an array item.value already exists
       value = item.value
       if item.value

+ 29 - 7
app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee

@@ -32,7 +32,7 @@ class App.UiElement.ApplicationSelector
       '^multiselect$': [__('contains all'), __('contains one'), __('contains all not'), __('contains one not')]
       '^tree_select$': [__('is'), __('is not')]
       '^multi_tree_select$': [__('contains all'), __('contains one'), __('contains all not'), __('contains one not')]
-      '^input$': [__('contains'), __('contains not'), __('is'), __('is not'), __('starts with'), __('ends with')]
+      '^input$': [__('contains'), __('contains not'), __('is any of'), __('is none of'), __('starts with one of'), __('ends with one of')]
       '^richtext$': [__('contains'), __('contains not')]
       '^textarea$': [__('contains'), __('contains not')]
       '^tag$': [__('contains all'), __('contains one'), __('contains all not'), __('contains one not')]
@@ -49,7 +49,7 @@ class App.UiElement.ApplicationSelector
         '^multiselect$': [__('contains all'), __('contains one'), __('contains all not'), __('contains one not')]
         '^tree_select$': [__('is'), __('is not'), __('has changed')]
         '^multi_tree_select$': [__('contains all'), __('contains one'), __('contains all not'), __('contains one not')]
-        '^input$': [__('contains'), __('contains not'), __('has changed'), __('is'), __('is not'), __('starts with'), __('ends with')]
+        '^input$': [__('contains'), __('contains not'), __('has changed'), __('is any of'), __('is none of'), __('starts with one of'), __('ends with one of')]
         '^richtext$': [__('contains'), __('contains not'), __('has changed')]
         '^textarea$': [__('contains'), __('contains not'), __('has changed')]
         '^tag$': [__('contains all'), __('contains one'), __('contains all not'), __('contains one not')]
@@ -438,6 +438,10 @@ class App.UiElement.ApplicationSelector
     selection = $("<select class=\"form-control\" name=\"#{name}\"></select>")
 
     attributeConfig = elements[groupAndAttribute]
+
+    # Compatibility layer for renamed operators (#4709).
+    meta.operator = @migrateOperator(attributeConfig, meta.operator)
+
     if attributeConfig.operator
 
       # check if operator exists
@@ -545,8 +549,9 @@ class App.UiElement.ApplicationSelector
   @buildValueConfigValue: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
     return _.clone(attribute.value[groupAndAttribute]['value'])
 
-  @buildValueName: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
-    return "#{attribute.name}::#{groupAndAttribute}::value"
+  @buildValueName: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute, valueType) ->
+    prefix = if valueType then "{#{valueType}}" else ''
+    return "#{prefix}#{attribute.name}::#{groupAndAttribute}::value"
 
   @buildValue: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
     # build new item
@@ -558,10 +563,14 @@ class App.UiElement.ApplicationSelector
     if config.relation is 'Organization'
       config.tag = 'autocompletion_ajax'
 
+    if config.tag and @tokenfieldTagRegex() and config.tag.match(@tokenfieldTagRegex()) and _.contains(['is any of', 'is none of', 'starts with one of', 'ends with one of'], meta.operator)
+      config.tag = 'tokenfield'
+
     # render ui element
     item = ''
     if config && App.UiElement[config.tag] && meta.operator isnt 'today'
-      config = @buildValueConfigNameValue(config, elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
+      { valueType } = App.UiElement[config.tag]
+      config = @buildValueConfigNameValue(config, elementFull, elementRow, groupAndAttribute, elements, meta, attribute, valueType)
 
       if 'multiple' of config
         config = @buildValueConfigMultiple(config, meta)
@@ -596,8 +605,8 @@ class App.UiElement.ApplicationSelector
     else
       elementRow.find('.js-value').removeClass('hide')
 
-  @buildValueConfigNameValue: (config, elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
-    config['name'] = @buildValueName(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
+  @buildValueConfigNameValue: (config, elementFull, elementRow, groupAndAttribute, elements, meta, attribute, valueType) ->
+    config['name'] = @buildValueName(elementFull, elementRow, groupAndAttribute, elements, meta, attribute, valueType)
     if attribute.value && attribute.value[groupAndAttribute]
       config['value'] = @buildValueConfigValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
 
@@ -677,3 +686,16 @@ class App.UiElement.ApplicationSelector
       enabled = true
       break
     return enabled
+
+  @tokenfieldTagRegex: ->
+    new RegExp('^input$', 'i')
+
+  @migrateOperator: (attributeConfig, operator) ->
+    if attributeConfig.tag and @tokenfieldTagRegex() and attributeConfig.tag.match(@tokenfieldTagRegex())
+      switch operator
+        when 'is' then return 'is any of'
+        when 'is not' then return 'is none of'
+        when 'starts with' then return 'starts with one of'
+        when 'ends with' then return 'ends with one of'
+
+    operator

+ 2 - 1
app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee

@@ -72,7 +72,8 @@ class App.UiElement.core_workflow_condition extends App.UiElement.ApplicationSel
       '^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')]
       '^multi_tree_select$': [__('contains'), __('contains not'), __('contains all'), __('contains all not'), __('is set'), __('not set'), __('has changed'), __('changed to')]
-      '^(input|textarea|richtext)$': [__('is'), __('is not'), __('starts with'), __('ends with'), __('matches regex'), __('does not match regex'), __('is set'), __('not set'), __('has changed'), __('changed to')]
+      '^input$': [__('is any of'), __('is none of'), __('starts with one of'), __('ends with one of'), __('matches regex'), __('does not match regex'), __('is set'), __('not set'), __('has changed'), __('changed to')]
+      '^(textarea|richtext)$': [__('is'), __('is not'), __('starts with'), __('ends with'), __('matches regex'), __('does not match regex'), __('is set'), __('not set'), __('has changed'), __('changed to')]
       '^tag$': [__('contains all'), __('contains one'), __('contains all not'), __('contains one not')]
 
     operatorsName =

+ 6 - 2
app/assets/javascripts/app/controllers/_ui_element/core_workflow_perform.coffee

@@ -119,9 +119,10 @@ class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelec
     currentOperator = elementRow.find('.js-operator option:selected').attr('value')
     return _.clone(attribute.value[groupAndAttribute][currentOperator])
 
-  @buildValueName: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
+  @buildValueName: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute, valueType) ->
+    prefix = if valueType then "{#{valueType}}" else ''
     currentOperator = elementRow.find('.js-operator option:selected').attr('value')
-    return "#{attribute.name}::#{groupAndAttribute}::#{currentOperator}"
+    return "#{prefix}#{attribute.name}::#{groupAndAttribute}::#{currentOperator}"
 
   @buildValue: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
     currentOperator = elementRow.find('.js-operator option:selected').attr('value')
@@ -178,3 +179,6 @@ class App.UiElement.core_workflow_perform extends App.UiElement.ApplicationSelec
 
   @hasDuplicateSelector: ->
     return true
+
+  @tokenfieldTagRegex: ->
+    false

+ 16 - 10
app/assets/javascripts/app/controllers/_ui_element/postmaster_match.coffee

@@ -164,17 +164,20 @@ class App.UiElement.postmaster_match
     item.find('.js-attributeSelector select').on('change', (e) =>
       key = $(e.target).find('option:selected').attr('value')
       elementRow = $(e.target).closest('.js-filterElement')
+      operator = elementRow.find('.js-operator select option:selected').attr('value')
+      value = elementRow.find('.js-value input').val()
       @rebuildAttributeSelectors(item, elementRow, key, attribute)
-      @rebuildOperater(item, elementRow, key, groups, undefined, attribute)
-      @buildValue(item, elementRow, key, groups, undefined, undefined, attribute)
+      @rebuildOperater(item, elementRow, key, groups, operator, attribute)
+      @buildValue(item, elementRow, key, groups, value, operator, attribute)
     )
 
     # change operator
-    item.find('.js-operator select').on('change', (e) =>
-      key = $(e.target).find('.js-attributeSelector option:selected').attr('value')
-      operator = $(e.target).find('option:selected').attr('value')
+    item.on('change', '.js-operator select', (e) =>
       elementRow = $(e.target).closest('.js-filterElement')
-      @buildValue(item, elementRow, key, groups, undefined, operator, attribute)
+      key = elementRow.find('.js-attributeSelector option:selected').attr('value')
+      operator = $(e.target).find('option:selected').attr('value')
+      value = elementRow.find('.js-value input').val()
+      @buildValue(item, elementRow, key, groups, value, operator, attribute)
     )
 
     # build initial params
@@ -207,15 +210,18 @@ class App.UiElement.postmaster_match
     item
 
   @buildValue: (elementFull, elementRow, key, groups, value, operator, attribute) ->
-
-    # do nothing if item already exists
     name = "#{attribute.name}::#{key}::value"
-    return if elementRow.find("[name=\"#{name}\"]").get(0)
+
     config =
       name: name
       tag: 'input'
       type: 'text'
       value: value
+
+    if _.contains(['is any of', 'is none of', 'starts with one of', 'ends with one of'], operator)
+      config.name = "{json}#{config.name}"
+      config.tag = 'tokenfield'
+
     item = App.UiElement[config.tag].render(config, {})
     elementRow.find('.js-value').html(item)
 
@@ -254,7 +260,7 @@ class App.UiElement.postmaster_match
   @buildOperator: (elementFull, elementRow, key, groups, current_operator, attribute) ->
     selection = $("<select class=\"form-control\" name=\"#{attribute.name}::#{key}::operator\"></select>")
 
-    for operator in ['contains', 'contains not', 'is', 'is not', 'starts with', 'ends with', 'matches regex', 'does not match regex']
+    for operator in ['contains', 'contains not', 'is any of', 'is none of', 'starts with one of', 'ends with one of', 'matches regex', 'does not match regex']
       operatorName = App.i18n.translateInline(operator)
       selected = ''
       if current_operator is operator

+ 14 - 1
app/assets/javascripts/app/controllers/_ui_element/ticket_selector.coffee

@@ -549,6 +549,10 @@ class App.UiElement.ticket_selector extends App.UiElement.ApplicationSelector
     selection = $('<select class="form-control"></select>')
 
     attributeConfig = elements[groupAndAttribute]
+
+    # Compatibility layer for renamed operators (#4709).
+    meta.operator = @migrateOperator(attributeConfig, meta.operator)
+
     if attributeConfig.operator
 
       # check if operator exists
@@ -649,7 +653,7 @@ class App.UiElement.ticket_selector extends App.UiElement.ApplicationSelector
     @buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
     toggleValue()
 
-  @buildValueConfigNameValue: (config, elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
+  @buildValueConfigNameValue: (config, elementFull, elementRow, groupAndAttribute, elements, meta, attribute, valueType) ->
     if !@hasExpertConditions() or !@isExpertMode
       return super
 
@@ -747,6 +751,15 @@ class App.UiElement.ticket_selector extends App.UiElement.ApplicationSelector
       value.value = element.find('.js-value .js-objectId').val()
     else if element.find('.js-value .js-shadow')?.val()
       value.value = element.find('.js-value .js-shadow').val()
+    else if element.find('[data-value]').length
+      valueField = element.find('[data-value]')
+      if valueField.data('valueType') is 'json'
+        try
+          value.value = JSON.parse(valueField.data('value'))
+        catch
+          App.Log.error 'App.UiElement.ticket_selector', 'Invalid JSON value for a subfield', valueField
+      else
+        value.value = valueField.data('value')
     else if element.find('.js-value input.form-control')?.val()
       value.value = element.find('.js-value input.form-control').val()
     else if element.find('.js-value .form-control')?.val()

+ 5 - 0
app/assets/javascripts/app/controllers/_ui_element/time_accounting_condition.coffee

@@ -0,0 +1,5 @@
+# coffeelint: disable=camel_case_classes
+class App.UiElement.time_accounting_condition extends App.UiElement.core_workflow_condition
+  @hasEmptySelectorAtStart: ->
+    return false
+

+ 0 - 5
app/assets/javascripts/app/controllers/_ui_element/time_accouting_condition.coffee

@@ -1,5 +0,0 @@
-# coffeelint: disable=camel_case_classes
-class App.UiElement.time_accouting_condition extends App.UiElement.core_workflow_condition
-  @hasEmptySelectorAtStart: ->
-    return false
-

+ 51 - 0
app/assets/javascripts/app/controllers/_ui_element/tokenfield.coffee

@@ -0,0 +1,51 @@
+# coffeelint: disable=camel_case_classes
+class App.UiElement.tokenfield
+  @valueType: 'json'
+
+  @render: (attributeConfig) ->
+    attribute = $.extend(true, {}, attributeConfig)
+
+    if !attribute.id
+      attribute.id = 'tokenfield-' + new Date().getTime() + '-' + Math.floor(Math.random() * 999999)
+
+    item = $( App.view('generic/input')(attribute: attribute) )
+      .attr('data-value-type', @valueType)
+      .data('value-type', @valueType)
+
+    if not _.isNull(attribute.value) or not _.isUndefined(attribute.value)
+
+      # Compatibility layer for renamed operators (#4709).
+      if not _.isArray(attribute.value)
+        attribute.value = [attribute.value]
+
+      value = @setValue(item, attribute.value)
+
+    callback = =>
+      item.tokenfield(
+        beautify: false
+        createTokensOnBlur: true
+        delimiter: ''
+        tokens: value or []
+      )
+        .on('tokenfield:createdtoken', => @updateValue(item))
+        .on('tokenfield:editedtoken', => @updateValue(item))
+        .on('tokenfield:removedtoken', => @updateValue(item))
+
+      item.parent().css('height', 'auto')
+
+    App.Delay.set(callback, 500, undefined, 'token')
+
+    item
+
+  @updateValue: (item) =>
+    tokens = item.tokenfield('getTokens')
+    @setValue(item, tokens.map((token) -> token.value))
+
+    true
+
+  @setValue: (item, newValue) ->
+    jsonValue = JSON.stringify(newValue)
+    item.attr('data-value', jsonValue)
+      .data('value', jsonValue)
+
+    newValue

+ 1 - 1
app/assets/javascripts/app/controllers/time_accounting_settings.coffee

@@ -23,7 +23,7 @@ class App.TimeAccountingSettings extends App.Controller
     )
 
     configure_attributes = [
-      { name: 'condition',  display: __('Conditions for affected objects'), tag: 'time_accouting_condition', workflow_object: 'Ticket', disable_operators: ['has changed', 'changed to'], null: false, preview: false },
+      { name: 'condition',  display: __('Conditions for affected objects'), tag: 'time_accounting_condition', workflow_object: 'Ticket', disable_operators: ['has changed', 'changed to'], null: false, preview: false },
     ]
 
     filter_params = App.Setting.get('time_accounting_selector')

Some files were not shown because too many files changed in this diff