Browse Source

Fixes #4206 - Cannot set date for pending close status in postmaster filter.

Rolf Schmidt 2 years ago
parent
commit
c89cb884e5

+ 597 - 0
app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee

@@ -0,0 +1,597 @@
+# coffeelint: disable=camel_case_classes
+
+###
+
+UI Element options:
+
+**attribute.notification**
+
+- Allows to send notifications (default: false)
+
+**attribute.ticket_delete**
+
+- Allows to delete the ticket (default: false)
+
+**attribute.user_action**
+
+- Allows pre conditions like current_user.id or user session specific values (default: true)
+
+###
+
+class App.UiElement.ApplicationAction
+  @defaults: (attribute) ->
+    defaults = ['ticket.state_id']
+
+    groups =
+      ticket:
+        name: __('Ticket')
+        model: 'Ticket'
+      article:
+        name: __('Article')
+        model: 'Article'
+
+    if attribute.notification
+      groups.notification =
+        name: __('Notification')
+        model: 'Notification'
+
+    # merge config
+    elements = {}
+    for groupKey, groupMeta of groups
+      if !groupMeta.model || !App[groupMeta.model]
+        if groupKey is 'notification'
+          elements["#{groupKey}.email"] = { name: 'email', display: __('Email') }
+          elements["#{groupKey}.sms"] = { name: 'sms', display: __('SMS') }
+          elements["#{groupKey}.webhook"] = { name: 'webhook', display: __('Webhook') }
+        else if groupKey is 'article'
+          elements["#{groupKey}.note"] = { name: 'note', display: __('Note') }
+      else
+
+        for row in App[groupMeta.model].configure_attributes
+
+          # ignore passwords and relations
+          if row.type isnt 'password' && row.name.substr(row.name.length-4,4) isnt '_ids'
+
+            # ignore readonly attributes
+            if !row.readonly
+              config = _.clone(row)
+
+              switch config.tag
+                when 'datetime'
+                  config.operator = ['static', 'relative']
+                when 'tag'
+                  config.operator = ['add', 'remove']
+
+              elements["#{groupKey}.#{config.name}"] = config
+
+    # add ticket deletion action
+    if attribute.ticket_delete
+      elements['ticket.action'] =
+        name: 'action'
+        display: __('Action')
+        tag: 'select'
+        null: false
+        translate: true
+        options:
+          delete: 'Delete'
+
+    [defaults, groups, elements]
+
+  @placeholder: (elementFull, attribute, params, groups, elements) ->
+    item = $( App.view('generic/ticket_perform_action/row')( attribute: attribute ) )
+    selector = @buildAttributeSelector(elementFull, groups, elements)
+    item.find('.js-attributeSelector').prepend(selector)
+    item
+
+  @render: (attribute, params = {}) ->
+
+    [defaults, groups, elements] = @defaults(attribute)
+
+    # return item
+    item = $( App.view('generic/ticket_perform_action/index')( attribute: attribute ) )
+
+    # add filter
+    item.on('click', '.js-rowActions .js-add', (e) =>
+      element = $(e.target).closest('.js-filterElement')
+      placeholder = @placeholder(item, attribute, params, groups, elements)
+      if element.get(0)
+        element.after(placeholder)
+      else
+        item.append(placeholder)
+      placeholder.find('.js-attributeSelector select').trigger('change')
+      @updateAttributeSelectors(item)
+    )
+
+    # remove filter
+    item.on('click', '.js-rowActions .js-remove', (e) =>
+      return if $(e.currentTarget).hasClass('is-disabled')
+      $(e.target).closest('.js-filterElement').remove()
+      @updateAttributeSelectors(item)
+    )
+
+    # change attribute selector
+    item.on('change', '.js-attributeSelector select', (e) =>
+      elementRow = $(e.target).closest('.js-filterElement')
+      groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value')
+      @rebuildAttributeSelectors(item, elementRow, groupAndAttribute, elements, {}, attribute)
+      @updateAttributeSelectors(item)
+    )
+
+    # change operator selector
+    item.on('change', '.js-operator select', (e) =>
+      elementRow = $(e.target).closest('.js-filterElement')
+      groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value')
+      @buildOperator(item, elementRow, groupAndAttribute, elements, {}, attribute)
+    )
+
+    # build initial params
+    if _.isEmpty(params[attribute.name])
+
+      for groupAndAttribute in defaults
+
+        # build and append
+        element = @placeholder(item, attribute, params, groups, elements)
+        item.append(element)
+        @rebuildAttributeSelectors(item, element, groupAndAttribute, elements, {}, attribute)
+
+    else
+
+      for groupAndAttribute, meta of params[attribute.name]
+
+        # build and append
+        element = @placeholder(item, attribute, params, groups, elements)
+        @rebuildAttributeSelectors(item, element, groupAndAttribute, elements, meta, attribute)
+        item.append(element)
+
+    @disableRemoveForOneAttribute(item)
+    item
+
+  @elementKeyGroup: (elementKey) ->
+    elementKey.split(/\./)[0]
+
+  @buildAttributeSelector: (elementFull, groups, elements) ->
+
+    # find first possible attribute
+    selectedValue = ''
+    elementFull.find('.js-attributeSelector select option').each(->
+      if !selectedValue && !$(@).prop('disabled')
+        selectedValue = $(@).val()
+    )
+
+    selection = $('<select class="form-control"></select>')
+    for groupKey, groupMeta of groups
+      displayName = App.i18n.translateInline(groupMeta.name)
+      selection.closest('select').append("<optgroup label=\"#{displayName}\" class=\"js-#{groupKey}\"></optgroup>")
+      optgroup = selection.find("optgroup.js-#{groupKey}")
+      for elementKey, elementGroup of elements
+        elementGroup = @elementKeyGroup(elementKey)
+        if elementGroup is groupKey
+          attributeConfig = elements[elementKey]
+          displayName = App.i18n.translateInline(attributeConfig.display)
+
+          selected = ''
+          if elementKey is selectedValue
+            selected = 'selected="selected"'
+          optgroup.append("<option value=\"#{elementKey}\" #{selected}>#{displayName}</option>")
+    selection
+
+  # disable - if we only have one attribute
+  @disableRemoveForOneAttribute: (elementFull) ->
+    if elementFull.find('.js-attributeSelector select').length > 1
+      elementFull.find('.js-remove').removeClass('is-disabled')
+    else
+      elementFull.find('.js-remove').addClass('is-disabled')
+
+  @updateAttributeSelectors: (elementFull) ->
+
+    # enable all
+    elementFull.find('.js-attributeSelector select option').prop('disabled', false)
+
+    # disable all used attributes
+    elementFull.find('.js-attributeSelector select').each(->
+      keyLocal = $(@).val()
+      elementFull.find('.js-attributeSelector select option[value="' + keyLocal + '"]').attr('disabled', true)
+    )
+
+    # disable - if we only have one attribute
+    @disableRemoveForOneAttribute(elementFull)
+
+  @rebuildAttributeSelectors: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
+
+    # set attribute
+    if groupAndAttribute
+      elementRow.find('.js-attributeSelector select').val(groupAndAttribute)
+
+    notificationTypeMatch = groupAndAttribute.match(/^notification.([\w]+)$/)
+    articleTypeMatch = groupAndAttribute.match(/^article.([\w]+)$/)
+
+    if _.isArray(notificationTypeMatch) && notificationType = notificationTypeMatch[1]
+      elementRow.find('.js-setAttribute').html('').addClass('hide')
+      elementRow.find('.js-setArticle').html('').addClass('hide')
+      @buildNotificationArea(notificationType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
+    else if _.isArray(articleTypeMatch) && articleType = articleTypeMatch[1]
+      elementRow.find('.js-setAttribute').html('').addClass('hide')
+      elementRow.find('.js-setNotification').html('').addClass('hide')
+      @buildArticleArea(articleType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
+    else
+      elementRow.find('.js-setNotification').html('').addClass('hide')
+      elementRow.find('.js-setArticle').html('').addClass('hide')
+      if !elementRow.find('.js-setAttribute div').get(0)
+        attributeSelectorElement = $( App.view('generic/ticket_perform_action/attribute_selector')(
+          attribute: attribute
+          name: name
+          meta: meta || {}
+        ))
+        elementRow.find('.js-setAttribute').html(attributeSelectorElement).removeClass('hide')
+      @buildOperator(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
+
+  @buildOperator: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
+    currentOperator = elementRow.find('.js-operator option:selected').attr('value')
+
+    if !meta.operator
+      meta.operator = currentOperator
+
+    name = "#{attribute.name}::#{groupAndAttribute}::operator"
+
+    selection = $("<select class=\"form-control\" name=\"#{name}\"></select>")
+    attributeConfig = elements[groupAndAttribute]
+    if !attributeConfig || !attributeConfig.operator
+      elementRow.find('.js-operator').parent().addClass('hide')
+    else
+      elementRow.find('.js-operator').parent().removeClass('hide')
+    if attributeConfig && attributeConfig.operator
+      for operator in attributeConfig.operator
+        operatorName = App.i18n.translateInline(operator)
+        selected = ''
+        if meta.operator is operator
+          selected = 'selected="selected"'
+        selection.append("<option value=\"#{operator}\" #{selected}>#{operatorName}</option>")
+      selection
+
+    elementRow.find('.js-operator select').replaceWith(selection)
+
+    @buildPreCondition(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
+
+  @buildPreCondition: (elementFull, elementRow, groupAndAttribute, elements, meta, attributeConfig) ->
+    currentOperator = elementRow.find('.js-operator option:selected').attr('value')
+    currentPreCondition = elementRow.find('.js-preCondition option:selected').attr('value')
+
+    if !meta.pre_condition
+      meta.pre_condition = currentPreCondition
+
+    toggleValue = =>
+      preCondition = elementRow.find('.js-preCondition option:selected').attr('value')
+      if preCondition isnt 'specific'
+        elementRow.find('.js-value select').html('')
+        elementRow.find('.js-value').addClass('hide')
+      else
+        elementRow.find('.js-value').removeClass('hide')
+        @buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
+
+    # force to use auto complition on user lookup
+    attribute = clone(attributeConfig, true)
+
+    name = "#{attribute.name}::#{groupAndAttribute}::value"
+    attributeSelected = elements[groupAndAttribute]
+
+    preCondition = false
+    if attributeSelected.relation is 'User'
+      preCondition = 'user'
+      attribute.tag = 'user_autocompletion'
+    if attributeSelected.relation is 'Organization'
+      preCondition = 'org'
+      attribute.tag = 'autocompletion_ajax'
+    if !preCondition || attribute.user_action is false
+      elementRow.find('.js-preCondition select').html('')
+      elementRow.find('.js-preCondition').closest('.controls').addClass('hide')
+      toggleValue()
+      @buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
+      return
+
+    elementRow.find('.js-preCondition').closest('.controls').removeClass('hide')
+    name = "#{attribute.name}::#{groupAndAttribute}::pre_condition"
+
+    selection = $("<select class=\"form-control\" name=\"#{name}\" ></select>")
+    options = {}
+    if preCondition is 'user'
+      options =
+        'current_user.id': App.i18n.translateInline('current user')
+        'specific': App.i18n.translateInline('specific user')
+
+      if attributeSelected.null is true
+        options['not_set'] = App.i18n.translateInline('unassign user')
+
+    else if preCondition is 'org'
+      options =
+        'current_user.organization_id': App.i18n.translateInline('current user organization')
+        'specific': App.i18n.translateInline('specific organization')
+
+    for key, value of options
+      selected = ''
+      if key is meta.pre_condition
+        selected = 'selected="selected"'
+      selection.append("<option value=\"#{key}\" #{selected}>#{App.i18n.translateInline(value)}</option>")
+    elementRow.find('.js-preCondition').closest('.controls').removeClass('hide')
+    elementRow.find('.js-preCondition select').replaceWith(selection)
+
+    elementRow.find('.js-preCondition select').on('change', (e) ->
+      toggleValue()
+    )
+
+    @buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
+    toggleValue()
+
+  @buildValue: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
+    name = "#{attribute.name}::#{groupAndAttribute}::value"
+
+    # build new item
+    attributeConfig = elements[groupAndAttribute]
+    config = clone(attributeConfig, true)
+
+    if config.relation is 'User'
+      config.tag = 'user_autocompletion'
+    if config.relation is 'Organization'
+      config.tag = 'autocompletion_ajax'
+
+    # render ui element
+    item = ''
+    if config && App.UiElement[config.tag]
+      config['name'] = name
+      if attribute.value && attribute.value[groupAndAttribute]
+        config['value'] = _.clone(attribute.value[groupAndAttribute]['value'])
+      config.multiple = false
+      config.default = undefined
+      config.nulloption = config.null
+      if config.tag is 'multiselect' || config.tag is 'multi_tree_select'
+        config.multiple = true
+      if config.tag is 'checkbox'
+        config.tag = 'select'
+      if config.tag is 'datetime'
+        config.validationContainer = 'self'
+      item = App.UiElement[config.tag].render(config, {})
+
+    relative_operators = [
+      __('before (relative)'),
+      __('within next (relative)'),
+      __('within last (relative)'),
+      __('after (relative)'),
+      __('till (relative)'),
+      __('from (relative)'),
+      __('relative'),
+    ]
+
+    upcoming_operator = meta.operator
+
+    if !_.include(config.operator, upcoming_operator)
+      if Array.isArray(config.operator)
+        upcoming_operator = config.operator[0]
+      else
+        upcoming_operator = null
+
+    if _.include(relative_operators, upcoming_operator)
+      config['name'] = "#{attribute.name}::#{groupAndAttribute}"
+      if attribute.value && attribute.value[groupAndAttribute]
+        config['value'] = _.clone(attribute.value[groupAndAttribute])
+      item = App.UiElement['time_range'].render(config, {})
+
+    elementRow.find('.js-setAttribute > .flex > .js-value').removeClass('hide').html(item)
+
+  @buildNotificationArea: (notificationType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
+
+    return if elementRow.find(".js-setNotification .js-body-#{notificationType}").get(0)
+
+    elementRow.find('.js-setNotification').empty()
+
+    options =
+      'article_last_sender': __('Sender of last article')
+      'ticket_owner': __('Owner')
+      'ticket_customer': __('Customer')
+      'ticket_agents': __('All agents')
+
+    name = "#{attribute.name}::notification.#{notificationType}"
+
+    messageLength = switch notificationType
+      when 'sms' then 160
+      else 200000
+
+    # meta.recipient was a string in the past (single-select) so we convert it to array if needed
+    if !_.isArray(meta.recipient)
+      meta.recipient = [meta.recipient]
+
+    columnSelectOptions = []
+    for key, value of options
+      selected = undefined
+      for recipient in meta.recipient
+        if key is recipient
+          selected = true
+      columnSelectOptions.push({ value: key, name: App.i18n.translatePlain(value), selected: selected })
+
+    columnSelectRecipientUserOptions = []
+    for user in App.User.all()
+      key = "userid_#{user.id}"
+      selected = undefined
+      for recipient in meta.recipient
+        if key is recipient
+          selected = true
+      columnSelectRecipientUserOptions.push({ value: key, name: "#{user.firstname} #{user.lastname}", selected: selected })
+
+    columnSelectRecipient = new App.ColumnSelect
+      attribute:
+        name:    "#{name}::recipient"
+        options: [
+          {
+            label: __('Variables'),
+            group: columnSelectOptions
+          },
+          {
+            label: __('User'),
+            group: columnSelectRecipientUserOptions
+          },
+        ]
+
+    selectionRecipient = columnSelectRecipient.element()
+
+    if notificationType is 'webhook'
+      notificationElement = $( App.view('generic/ticket_perform_action/webhook')(
+        attribute: attribute
+        name: name
+        notificationType: notificationType
+        meta: meta || {}
+      ))
+
+      notificationElement.find('.js-recipient select').replaceWith(selectionRecipient)
+
+
+      if App.Webhook.search(filter: { active: true }).length isnt 0 || !_.isEmpty(meta.webhook_id)
+        webhookSelection = App.UiElement.select.render(
+          name: "#{name}::webhook_id"
+          multiple: false
+          null: false
+          relation: 'Webhook'
+          value: meta.webhook_id
+          translate: false
+          nulloption: true
+        )
+      else
+        webhookSelection = App.view('generic/ticket_perform_action/webhook_not_available')( attribute: attribute )
+
+      notificationElement.find('.js-webhooks').html(webhookSelection)
+
+    else
+      notificationElement = $( App.view('generic/ticket_perform_action/notification')(
+        attribute: attribute
+        name: name
+        notificationType: notificationType
+        meta: meta || {}
+      ))
+
+      notificationElement.find('.js-recipient select').replaceWith(selectionRecipient)
+
+      visibilitySelection = App.UiElement.select.render(
+        name: "#{name}::internal"
+        multiple: false
+        null: false
+        options: { true: __('internal'), false: __('public') }
+        value: meta.internal || 'false'
+        translate: true
+      )
+
+      includeAttachmentsCheckbox = App.UiElement.select.render(
+        name: "#{name}::include_attachments"
+        multiple: false
+        null: false
+        options: { true: __('Yes'), false: __('No') }
+        value: meta.include_attachments || 'false'
+        translate: true
+      )
+
+      notificationElement.find('.js-internal').html(visibilitySelection)
+      notificationElement.find('.js-include_attachments').html(includeAttachmentsCheckbox)
+
+      notificationElement.find('.js-body div[contenteditable="true"]').ce(
+        mode: 'richtext'
+        placeholder: __('message')
+        maxlength: messageLength
+      )
+      new App.WidgetPlaceholder(
+        el: notificationElement.find('.js-body div[contenteditable="true"]').parent()
+        objects: [
+          {
+            prefix: 'ticket'
+            object: 'Ticket'
+            display: __('Ticket')
+          },
+          {
+            prefix: 'article'
+            object: 'TicketArticle'
+            display: __('Article')
+          },
+          {
+            prefix: 'user'
+            object: 'User'
+            display: __('Current User')
+          },
+        ]
+      )
+
+    elementRow.find('.js-setNotification').html(notificationElement).removeClass('hide')
+
+    if App.Config.get('smime_integration') == true
+      selection = App.UiElement.select.render(
+        name: "#{name}::sign"
+        multiple: false
+        options: {
+          'no': __('Do not sign email')
+          'discard': __('Sign email (if not possible, discard notification)')
+          'always': __('Sign email (if not possible, send notification anyway)')
+        }
+        value: meta.sign
+        translate: true
+      )
+
+      elementRow.find('.js-sign').html(selection)
+
+      selection = App.UiElement.select.render(
+        name: "#{name}::encryption"
+        multiple: false
+        options: {
+          'no': __('Do not encrypt email')
+          'discard': __('Encrypt email (if not possible, discard notification)')
+          'always': __('Encrypt email (if not possible, send notification anyway)')
+        }
+        value: meta.encryption
+        translate: true
+      )
+
+      elementRow.find('.js-encryption').html(selection)
+
+  @buildArticleArea: (articleType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
+
+    return if elementRow.find(".js-setArticle .js-body-#{articleType}").get(0)
+
+    elementRow.find('.js-setArticle').empty()
+
+    name = "#{attribute.name}::article.#{articleType}"
+    selection = App.UiElement.select.render(
+      name: "#{name}::internal"
+      multiple: false
+      null: false
+      label: __('Visibility')
+      options: { true: 'internal', false: 'public' }
+      value: meta.internal
+      translate: true
+    )
+    articleElement = $( App.view('generic/ticket_perform_action/article')(
+      attribute: attribute
+      name: name
+      articleType: articleType
+      meta: meta || {}
+    ))
+    articleElement.find('.js-internal').html(selection)
+    articleElement.find('.js-body div[contenteditable="true"]').ce(
+      mode: 'richtext'
+      placeholder: __('message')
+      maxlength: 200000
+    )
+    new App.WidgetPlaceholder(
+      el: articleElement.find('.js-body div[contenteditable="true"]').parent()
+      objects: [
+        {
+          prefix: 'ticket'
+          object: 'Ticket'
+          display: __('Ticket')
+        },
+        {
+          prefix: 'article'
+          object: 'TicketArticle'
+          display: __('Article')
+        },
+        {
+          prefix: 'user'
+          object: 'User'
+          display: __('Current User')
+        },
+      ]
+    )
+
+    elementRow.find('.js-setArticle').html(articleElement).removeClass('hide')

+ 21 - 250
app/assets/javascripts/app/controllers/_ui_element/postmaster_set.coffee

@@ -1,78 +1,20 @@
 # coffeelint: disable=camel_case_classes
-class App.UiElement.postmaster_set
-  @defaults: ->
+class App.UiElement.postmaster_set extends App.UiElement.ApplicationAction
+  @defaults: (attribute) ->
+    defaults = ['x-zammad-ticket-state_id']
+
     groups =
       ticket:
-        name: 'Ticket'
+        name: __('Ticket')
         model: 'Ticket'
-        options: [
-          {
-            value:    'priority_id'
-            name:     __('Priority')
-            relation: 'TicketPriority'
-          }
-          {
-            value:    'state_id'
-            name:     __('State')
-            relation: 'TicketState'
-          }
-          {
-            value:    'tags'
-            name:     __('Tag')
-            tag:      'tag'
-          }
-          {
-            value:    'customer_id'
-            name:     __('Customer')
-            relation: 'User'
-            tag:      'user_autocompletion'
-            disableCreateObject: true
-          }
-          {
-            value:    'group_id'
-            name:     __('Group')
-            relation: 'Group'
-          }
-          {
-            value:    'owner_id'
-            name:     __('Owner')
-            relation: 'User'
-            tag:      'user_autocompletion'
-            disableCreateObject: true
-          }
-        ]
       article:
         name: 'Article'
-        options: [
-          {
-            value:    'x-zammad-article-internal'
-            name:     __('Internal')
-            options:  { true: 'yes', false: 'no'}
-          }
-          {
-            value:    'x-zammad-article-type_id'
-            name:     __('Type')
-            relation: 'TicketArticleType'
-          }
-          {
-            value:    'x-zammad-article-sender_id'
-            name:     __('Sender')
-            relation: 'TicketArticleSender'
-          }
-        ]
       expert:
         name: 'Expert'
-        options: [
-          {
-            value:    'x-zammad-ignore'
-            name:     __('Ignore Message')
-            options:  { true: 'yes', false: 'no'}
-          }
-        ]
 
     elements = {}
     for groupKey, groupMeta of groups
-      if groupMeta.model && App[groupMeta.model]
+      if groupMeta.model
         for row in App[groupMeta.model].configure_attributes
 
           # ignore passwords and relations
@@ -81,195 +23,24 @@ class App.UiElement.postmaster_set
             # ignore readonly attributes
             if !row.readonly
               config = _.clone(row)
-              if config.tag is 'tag'
-                config.operator = ['add', 'remove']
-              elements["x-zammad-ticket-#{config.name}"] = config
-
-    # add additional ticket attributes
-    for row in App.Ticket.configure_attributes
-      exists = false
-      for item in groups.ticket.options
-        if item.value is row.name
-          exists = true
-
-        # do not support this types
-        else if row.tag is 'datetime' || row.tag is 'date' || row.tag is 'tag'
-          exists = true
-
-      # ignore passwords and relations
-      if !exists && row.type isnt 'password' && row.name.substr(row.name.length-4,4) isnt '_ids'
-
-        # ignore readonly attributes
-        if !row.readonly
-          item =
-            value:    row.name
-            name:     row.display
-            relation: row.relation
-            tag:      row.tag
-            options:  row.options
-          groups.ticket.options.push item
-
-    for item in groups.ticket.options
-      item.value = "x-zammad-ticket-#{item.value}"
-
-    [elements, groups]
-
-  @placeholder: (elementFull, attribute, params = {}, groups) ->
-    item = $( App.view('generic/postmaster_set_row')(attribute: attribute) )
-    selector = @buildAttributeSelector(elementFull, groups, attribute, item)
-    item.find('.js-attributeSelector').prepend(selector)
-    item
-
-  @render: (attribute, params = {}) ->
-
-    [elements, groups] = @defaults()
-
-    # scaffold of match elements
-    item = $( App.view('generic/postmaster_set')(attribute: attribute) )
-
-    # add filter
-    item.on('click', '.js-add', (e) =>
-      element = $(e.target).closest('.js-filterElement')
-      placeholder = @placeholder(item, attribute, params, groups)
-      if element.get(0)
-        element.after(placeholder)
-      else
-        item.append(placeholder)
-      placeholder.find('.js-attributeSelector select').trigger('change')
-    )
-
-    # remove filter
-    item.on('click', '.js-remove', (e) =>
-      return if $(e.currentTarget).hasClass('is-disabled')
-      $(e.target).closest('.js-filterElement').remove()
-      @rebuildAttributeSelectors(item)
-    )
-
-    # change attribute selector
-    item.on('change', '.js-attributeSelector select', (e) =>
-      elementRow = $(e.target).closest('.js-filterElement')
-      groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value')
-      @rebuildAttributeSelectors(item, elementRow, groupAndAttribute, attribute)
-      @buildOperator(item, elementRow, groupAndAttribute, elements, {},  attribute)
-      @buildValue(item, elementRow, groupAndAttribute, groups, undefined, undefined, attribute)
-    )
-
-    # build initial params
-    if _.isEmpty(params[attribute.name])
-      element = @placeholder(item, attribute, params, groups)
-      item.append(element)
-      groupAndAttribute = element.find('.js-attributeSelector option:selected').attr('value')
-      @rebuildAttributeSelectors(item, element, groupAndAttribute, attribute)
-      @buildOperator(item, element, groupAndAttribute, elements, {},  attribute)
-      @buildValue(item, element, groupAndAttribute, groups, undefined, undefined, attribute)
-      return item
-
-    else
-      for key, meta of params[attribute.name]
-        operator = meta.operator
-        value = meta.value
-
-        # build and append
-        element = @placeholder(item, attribute, params, groups)
-        groupAndAttribute = element.find('.js-attributeSelector option:selected').attr('value')
-        @rebuildAttributeSelectors(item, element, key, attribute)
-        @buildOperator(item, element, key, elements, {},  attribute)
-        @buildValue(item, element, key, groups, value, operator, attribute)
-
-        item.append(element)
-      item.find('.js-attributeSelector select').trigger('change')
-
-    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 = {}
-    for groupName, meta of groups
-      for entry in meta.options
-        if entry.value is key
-          config = clone(entry)
-    if !config.tag
-      if config.relation || config.options
-        config['tag'] = 'select'
-      else
-        config['tag'] = 'input'
-        config['type'] = 'text'
-    config['name'] = name
-    if !value && attribute.value && attribute.value[key]
-      config['value'] = attribute.value[key].value
-    else
-      config['value'] = value
-    item = App.UiElement[config.tag].render(config, {})
-    elementRow.find('.js-value').html(item)
-
-  @buildAttributeSelector: (elementFull, groups, attribute) ->
-
-    # find first possible attribute
-    selectedValue = ''
-    elementFull.find('.js-attributeSelector select option').each(->
-      if !selectedValue && !$(@).prop('disabled')
-        selectedValue = $(@).val()
-    )
-
-    selection = $('<select class="form-control"></select>')
-    for groupKey, groupMeta of groups
-      displayName = App.i18n.translateInline(groupMeta.name)
-      selection.closest('select').append("<optgroup label=\"#{displayName}\" class=\"js-#{groupKey}\"></optgroup>")
-      optgroup = selection.find("optgroup.js-#{groupKey}")
-      for entry in groupMeta.options
-        displayName = App.i18n.translateInline(entry.name)
-        selected = ''
-        if entry.value is selectedValue
-          selected = 'selected="selected"'
-        optgroup.append("<option value=\"#{entry.value}\" #{selected}>#{displayName}</option>")
-    selection
-
-  @rebuildAttributeSelectors: (elementFull, elementRow, key, attribute) ->
-
-    # enable all
-    elementFull.find('.js-attributeSelector select option').prop('disabled', false)
-
-    # disable all used attributes
-    elementFull.find('.js-attributeSelector select').each(->
-      keyLocal = $(@).val()
-      elementFull.find('.js-attributeSelector select option[value="' + keyLocal + '"]').attr('disabled', true)
-    )
-
-    # disable - if we only have one attribute
-    if elementFull.find('.js-attributeSelector select').length > 1
-      elementFull.find('.js-remove').removeClass('is-disabled')
-    else
-      elementFull.find('.js-remove').addClass('is-disabled')
-
-    # set attribute
-    if key
-      elementRow.find('.js-attributeSelector select').val(key)
 
-  @buildOperator: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
-    currentOperator = elementRow.find('.js-operator option:selected').attr('value')
+              switch config.tag
+                when 'datetime'
+                  config.operator = ['static', 'relative']
+                when 'tag'
+                  config.operator = ['add', 'remove']
 
-    if !meta.operator
-      meta.operator = currentOperator
+              elements["x-zammad-#{groupKey}-#{config.name}"] = config
 
-    name = "#{attribute.name}::#{groupAndAttribute}::operator"
+    elements['x-zammad-article-internal']  = _.clone(App.TicketArticle.attributesGet()['internal'])
+    elements['x-zammad-article-internal'].null = false
 
-    selection = $("<select class=\"form-control\" name=\"#{name}\"></select>")
-    attributeConfig = elements[groupAndAttribute]
+    elements['x-zammad-article-type_id']   = _.clone(App.TicketArticle.attributesGet()['type_id'])
+    elements['x-zammad-article-sender_id'] = _.clone(App.TicketArticle.attributesGet()['sender_id'])
+    elements['x-zammad-ignore'] = { name: 'x-zammad-ignore', display: __('Ignore Message'), tag: 'boolean', type: 'boolean', null: false }
 
-    if !attributeConfig || !attributeConfig.operator
-      elementRow.find('.js-operator').parent().addClass('hide')
-    else
-      elementRow.find('.js-operator').parent().removeClass('hide')
-    if attributeConfig && attributeConfig.operator
-      for operator in attributeConfig.operator
-        operatorName = App.i18n.translateInline(operator)
-        selected = ''
-        if meta.operator is operator
-          selected = 'selected="selected"'
-        selection.append("<option value=\"#{operator}\" #{selected}>#{operatorName}</option>")
-      selection
+    [defaults, groups, elements]
 
-    elementRow.find('.js-operator select').replaceWith(selection)
+  @elementKeyGroup: (elementKey) ->
+    return 'expert' if elementKey is 'x-zammad-ignore'
+    elementKey.replace('x-zammad-', '').split(/-/)[0]

+ 1 - 574
app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee

@@ -1,575 +1,2 @@
 # coffeelint: disable=camel_case_classes
-class App.UiElement.ticket_perform_action
-  @defaults: (attribute) ->
-    defaults = ['ticket.state_id']
-
-    groups =
-      ticket:
-        name: __('Ticket')
-        model: 'Ticket'
-      article:
-        name: __('Article')
-        model: 'Article'
-
-    if attribute.notification
-      groups.notification =
-        name: __('Notification')
-        model: 'Notification'
-
-    # merge config
-    elements = {}
-    for groupKey, groupMeta of groups
-      if !groupMeta.model || !App[groupMeta.model]
-        if groupKey is 'notification'
-          elements["#{groupKey}.email"] = { name: 'email', display: __('Email') }
-          elements["#{groupKey}.sms"] = { name: 'sms', display: __('SMS') }
-          elements["#{groupKey}.webhook"] = { name: 'webhook', display: __('Webhook') }
-        else if groupKey is 'article'
-          elements["#{groupKey}.note"] = { name: 'note', display: __('Note') }
-      else
-
-        for row in App[groupMeta.model].configure_attributes
-
-          # ignore passwords and relations
-          if row.type isnt 'password' && row.name.substr(row.name.length-4,4) isnt '_ids'
-
-            # ignore readonly attributes
-            if !row.readonly
-              config = _.clone(row)
-
-              switch config.tag
-                when 'datetime'
-                  config.operator = ['static', 'relative']
-                when 'tag'
-                  config.operator = ['add', 'remove']
-
-              elements["#{groupKey}.#{config.name}"] = config
-
-    # add ticket deletion action
-    if attribute.ticket_delete
-      elements['ticket.action'] =
-        name: 'action'
-        display: __('Action')
-        tag: 'select'
-        null: false
-        translate: true
-        options:
-          delete: 'Delete'
-
-    [defaults, groups, elements]
-
-  @placeholder: (elementFull, attribute, params, groups, elements) ->
-    item = $( App.view('generic/ticket_perform_action/row')( attribute: attribute ) )
-    selector = @buildAttributeSelector(elementFull, groups, elements)
-    item.find('.js-attributeSelector').prepend(selector)
-    item
-
-  @render: (attribute, params = {}) ->
-
-    [defaults, groups, elements] = @defaults(attribute)
-
-    # return item
-    item = $( App.view('generic/ticket_perform_action/index')( attribute: attribute ) )
-
-    # add filter
-    item.on('click', '.js-rowActions .js-add', (e) =>
-      element = $(e.target).closest('.js-filterElement')
-      placeholder = @placeholder(item, attribute, params, groups, elements)
-      if element.get(0)
-        element.after(placeholder)
-      else
-        item.append(placeholder)
-      placeholder.find('.js-attributeSelector select').trigger('change')
-      @updateAttributeSelectors(item)
-    )
-
-    # remove filter
-    item.on('click', '.js-rowActions .js-remove', (e) =>
-      return if $(e.currentTarget).hasClass('is-disabled')
-      $(e.target).closest('.js-filterElement').remove()
-      @updateAttributeSelectors(item)
-    )
-
-    # change attribute selector
-    item.on('change', '.js-attributeSelector select', (e) =>
-      elementRow = $(e.target).closest('.js-filterElement')
-      groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value')
-      @rebuildAttributeSelectors(item, elementRow, groupAndAttribute, elements, {}, attribute)
-      @updateAttributeSelectors(item)
-    )
-
-    # change operator selector
-    item.on('change', '.js-operator select', (e) =>
-      elementRow = $(e.target).closest('.js-filterElement')
-      groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value')
-      @buildOperator(item, elementRow, groupAndAttribute, elements, {}, attribute)
-    )
-
-    # build initial params
-    if _.isEmpty(params[attribute.name])
-
-      for groupAndAttribute in defaults
-
-        # build and append
-        element = @placeholder(item, attribute, params, groups, elements)
-        item.append(element)
-        @rebuildAttributeSelectors(item, element, groupAndAttribute, elements, {}, attribute)
-
-    else
-
-      for groupAndAttribute, meta of params[attribute.name]
-
-        # build and append
-        element = @placeholder(item, attribute, params, groups, elements)
-        @rebuildAttributeSelectors(item, element, groupAndAttribute, elements, meta, attribute)
-        item.append(element)
-
-    @disableRemoveForOneAttribute(item)
-    item
-
-  @buildAttributeSelector: (elementFull, groups, elements) ->
-
-    # find first possible attribute
-    selectedValue = ''
-    elementFull.find('.js-attributeSelector select option').each(->
-      if !selectedValue && !$(@).prop('disabled')
-        selectedValue = $(@).val()
-    )
-
-    selection = $('<select class="form-control"></select>')
-    for groupKey, groupMeta of groups
-      displayName = App.i18n.translateInline(groupMeta.name)
-      selection.closest('select').append("<optgroup label=\"#{displayName}\" class=\"js-#{groupKey}\"></optgroup>")
-      optgroup = selection.find("optgroup.js-#{groupKey}")
-      for elementKey, elementGroup of elements
-        spacer = elementKey.split(/\./)
-        if spacer[0] is groupKey
-          attributeConfig = elements[elementKey]
-          displayName = App.i18n.translateInline(attributeConfig.display)
-
-          selected = ''
-          if elementKey is selectedValue
-            selected = 'selected="selected"'
-          optgroup.append("<option value=\"#{elementKey}\" #{selected}>#{displayName}</option>")
-    selection
-
-  # disable - if we only have one attribute
-  @disableRemoveForOneAttribute: (elementFull) ->
-    if elementFull.find('.js-attributeSelector select').length > 1
-      elementFull.find('.js-remove').removeClass('is-disabled')
-    else
-      elementFull.find('.js-remove').addClass('is-disabled')
-
-  @updateAttributeSelectors: (elementFull) ->
-
-    # enable all
-    elementFull.find('.js-attributeSelector select option').prop('disabled', false)
-
-    # disable all used attributes
-    elementFull.find('.js-attributeSelector select').each(->
-      keyLocal = $(@).val()
-      elementFull.find('.js-attributeSelector select option[value="' + keyLocal + '"]').attr('disabled', true)
-    )
-
-    # disable - if we only have one attribute
-    @disableRemoveForOneAttribute(elementFull)
-
-  @rebuildAttributeSelectors: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
-
-    # set attribute
-    if groupAndAttribute
-      elementRow.find('.js-attributeSelector select').val(groupAndAttribute)
-
-    notificationTypeMatch = groupAndAttribute.match(/^notification.([\w]+)$/)
-    articleTypeMatch = groupAndAttribute.match(/^article.([\w]+)$/)
-
-    if _.isArray(notificationTypeMatch) && notificationType = notificationTypeMatch[1]
-      elementRow.find('.js-setAttribute').html('').addClass('hide')
-      elementRow.find('.js-setArticle').html('').addClass('hide')
-      @buildNotificationArea(notificationType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
-    else if _.isArray(articleTypeMatch) && articleType = articleTypeMatch[1]
-      elementRow.find('.js-setAttribute').html('').addClass('hide')
-      elementRow.find('.js-setNotification').html('').addClass('hide')
-      @buildArticleArea(articleType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
-    else
-      elementRow.find('.js-setNotification').html('').addClass('hide')
-      elementRow.find('.js-setArticle').html('').addClass('hide')
-      if !elementRow.find('.js-setAttribute div').get(0)
-        attributeSelectorElement = $( App.view('generic/ticket_perform_action/attribute_selector')(
-          attribute: attribute
-          name: name
-          meta: meta || {}
-        ))
-        elementRow.find('.js-setAttribute').html(attributeSelectorElement).removeClass('hide')
-      @buildOperator(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
-
-  @buildOperator: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
-    currentOperator = elementRow.find('.js-operator option:selected').attr('value')
-
-    if !meta.operator
-      meta.operator = currentOperator
-
-    name = "#{attribute.name}::#{groupAndAttribute}::operator"
-
-    selection = $("<select class=\"form-control\" name=\"#{name}\"></select>")
-    attributeConfig = elements[groupAndAttribute]
-    if !attributeConfig || !attributeConfig.operator
-      elementRow.find('.js-operator').parent().addClass('hide')
-    else
-      elementRow.find('.js-operator').parent().removeClass('hide')
-    if attributeConfig && attributeConfig.operator
-      for operator in attributeConfig.operator
-        operatorName = App.i18n.translateInline(operator)
-        selected = ''
-        if meta.operator is operator
-          selected = 'selected="selected"'
-        selection.append("<option value=\"#{operator}\" #{selected}>#{operatorName}</option>")
-      selection
-
-    elementRow.find('.js-operator select').replaceWith(selection)
-
-    @buildPreCondition(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
-
-  @buildPreCondition: (elementFull, elementRow, groupAndAttribute, elements, meta, attributeConfig) ->
-    currentOperator = elementRow.find('.js-operator option:selected').attr('value')
-    currentPreCondition = elementRow.find('.js-preCondition option:selected').attr('value')
-
-    if !meta.pre_condition
-      meta.pre_condition = currentPreCondition
-
-    toggleValue = =>
-      preCondition = elementRow.find('.js-preCondition option:selected').attr('value')
-      if preCondition isnt 'specific'
-        elementRow.find('.js-value select').html('')
-        elementRow.find('.js-value').addClass('hide')
-      else
-        elementRow.find('.js-value').removeClass('hide')
-        @buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
-
-    # force to use auto complition on user lookup
-    attribute = clone(attributeConfig, true)
-
-    name = "#{attribute.name}::#{groupAndAttribute}::value"
-    attributeSelected = elements[groupAndAttribute]
-
-    preCondition = false
-    if attributeSelected.relation is 'User'
-      preCondition = 'user'
-      attribute.tag = 'user_autocompletion'
-    if attributeSelected.relation is 'Organization'
-      preCondition = 'org'
-      attribute.tag = 'autocompletion_ajax'
-    if !preCondition
-      elementRow.find('.js-preCondition select').html('')
-      elementRow.find('.js-preCondition').closest('.controls').addClass('hide')
-      toggleValue()
-      @buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
-      return
-
-    elementRow.find('.js-preCondition').closest('.controls').removeClass('hide')
-    name = "#{attribute.name}::#{groupAndAttribute}::pre_condition"
-
-    selection = $("<select class=\"form-control\" name=\"#{name}\" ></select>")
-    options = {}
-    if preCondition is 'user'
-      options =
-        'current_user.id': App.i18n.translateInline('current user')
-        'specific': App.i18n.translateInline('specific user')
-
-      if attributeSelected.null is true
-        options['not_set'] = App.i18n.translateInline('unassign user')
-
-    else if preCondition is 'org'
-      options =
-        'current_user.organization_id': App.i18n.translateInline('current user organization')
-        'specific': App.i18n.translateInline('specific organization')
-
-    for key, value of options
-      selected = ''
-      if key is meta.pre_condition
-        selected = 'selected="selected"'
-      selection.append("<option value=\"#{key}\" #{selected}>#{App.i18n.translateInline(value)}</option>")
-    elementRow.find('.js-preCondition').closest('.controls').removeClass('hide')
-    elementRow.find('.js-preCondition select').replaceWith(selection)
-
-    elementRow.find('.js-preCondition select').on('change', (e) ->
-      toggleValue()
-    )
-
-    @buildValue(elementFull, elementRow, groupAndAttribute, elements, meta, attribute)
-    toggleValue()
-
-  @buildValue: (elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
-    name = "#{attribute.name}::#{groupAndAttribute}::value"
-
-    # build new item
-    attributeConfig = elements[groupAndAttribute]
-    config = clone(attributeConfig, true)
-
-    if config.relation is 'User'
-      config.tag = 'user_autocompletion'
-    if config.relation is 'Organization'
-      config.tag = 'autocompletion_ajax'
-
-    # render ui element
-    item = ''
-    if config && App.UiElement[config.tag]
-      config['name'] = name
-      if attribute.value && attribute.value[groupAndAttribute]
-        config['value'] = _.clone(attribute.value[groupAndAttribute]['value'])
-      config.multiple = false
-      config.default = undefined
-      config.nulloption = config.null
-      if config.tag is 'multiselect' || config.tag is 'multi_tree_select'
-        config.multiple = true
-      if config.tag is 'checkbox'
-        config.tag = 'select'
-      if config.tag is 'datetime'
-        config.validationContainer = 'self'
-      item = App.UiElement[config.tag].render(config, {})
-
-    relative_operators = [
-      'before (relative)',
-      'within next (relative)',
-      'within last (relative)',
-      'after (relative)',
-      'till (relative)',
-      'from (relative)',
-      'relative'
-    ]
-
-    upcoming_operator = meta.operator
-
-    if !_.include(config.operator, upcoming_operator)
-      if Array.isArray(config.operator)
-        upcoming_operator = config.operator[0]
-      else
-        upcoming_operator = null
-
-    if _.include(relative_operators, upcoming_operator)
-      config['name'] = "#{attribute.name}::#{groupAndAttribute}"
-      if attribute.value && attribute.value[groupAndAttribute]
-        config['value'] = _.clone(attribute.value[groupAndAttribute])
-      item = App.UiElement['time_range'].render(config, {})
-
-    elementRow.find('.js-setAttribute > .flex > .js-value').removeClass('hide').html(item)
-
-  @buildNotificationArea: (notificationType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
-
-    return if elementRow.find(".js-setNotification .js-body-#{notificationType}").get(0)
-
-    elementRow.find('.js-setNotification').empty()
-
-    options =
-      'article_last_sender': __('Sender of last article')
-      'ticket_owner': __('Owner')
-      'ticket_customer': __('Customer')
-      'ticket_agents': __('All agents')
-
-    name = "#{attribute.name}::notification.#{notificationType}"
-
-    messageLength = switch notificationType
-      when 'sms' then 160
-      else 200000
-
-    # meta.recipient was a string in the past (single-select) so we convert it to array if needed
-    if !_.isArray(meta.recipient)
-      meta.recipient = [meta.recipient]
-
-    columnSelectOptions = []
-    for key, value of options
-      selected = undefined
-      for recipient in meta.recipient
-        if key is recipient
-          selected = true
-      columnSelectOptions.push({ value: key, name: App.i18n.translatePlain(value), selected: selected })
-
-    columnSelectRecipientUserOptions = []
-    for user in App.User.all()
-      key = "userid_#{user.id}"
-      selected = undefined
-      for recipient in meta.recipient
-        if key is recipient
-          selected = true
-      columnSelectRecipientUserOptions.push({ value: key, name: "#{user.firstname} #{user.lastname}", selected: selected })
-
-    columnSelectRecipient = new App.ColumnSelect
-      attribute:
-        name:    "#{name}::recipient"
-        options: [
-          {
-            label: __('Variables'),
-            group: columnSelectOptions
-          },
-          {
-            label: __('User'),
-            group: columnSelectRecipientUserOptions
-          },
-        ]
-
-    selectionRecipient = columnSelectRecipient.element()
-
-    if notificationType is 'webhook'
-      notificationElement = $( App.view('generic/ticket_perform_action/webhook')(
-        attribute: attribute
-        name: name
-        notificationType: notificationType
-        meta: meta || {}
-      ))
-
-      notificationElement.find('.js-recipient select').replaceWith(selectionRecipient)
-
-
-      if App.Webhook.search(filter: { active: true }).length isnt 0 || !_.isEmpty(meta.webhook_id)
-        webhookSelection = App.UiElement.select.render(
-          name: "#{name}::webhook_id"
-          multiple: false
-          null: false
-          relation: 'Webhook'
-          value: meta.webhook_id
-          translate: false
-          nulloption: true
-        )
-      else
-        webhookSelection = App.view('generic/ticket_perform_action/webhook_not_available')( attribute: attribute )
-
-      notificationElement.find('.js-webhooks').html(webhookSelection)
-
-    else
-      notificationElement = $( App.view('generic/ticket_perform_action/notification')(
-        attribute: attribute
-        name: name
-        notificationType: notificationType
-        meta: meta || {}
-      ))
-
-      notificationElement.find('.js-recipient select').replaceWith(selectionRecipient)
-
-      visibilitySelection = App.UiElement.select.render(
-        name: "#{name}::internal"
-        multiple: false
-        null: false
-        options: { true: __('internal'), false: __('public') }
-        value: meta.internal || 'false'
-        translate: true
-      )
-
-      includeAttachmentsCheckbox = App.UiElement.select.render(
-        name: "#{name}::include_attachments"
-        multiple: false
-        null: false
-        options: { true: __('Yes'), false: __('No') }
-        value: meta.include_attachments || 'false'
-        translate: true
-      )
-
-      notificationElement.find('.js-internal').html(visibilitySelection)
-      notificationElement.find('.js-include_attachments').html(includeAttachmentsCheckbox)
-
-      notificationElement.find('.js-body div[contenteditable="true"]').ce(
-        mode: 'richtext'
-        placeholder: __('message')
-        maxlength: messageLength
-      )
-      new App.WidgetPlaceholder(
-        el: notificationElement.find('.js-body div[contenteditable="true"]').parent()
-        objects: [
-          {
-            prefix: 'ticket'
-            object: 'Ticket'
-            display: __('Ticket')
-          },
-          {
-            prefix: 'article'
-            object: 'TicketArticle'
-            display: __('Article')
-          },
-          {
-            prefix: 'user'
-            object: 'User'
-            display: __('Current User')
-          },
-        ]
-      )
-
-    elementRow.find('.js-setNotification').html(notificationElement).removeClass('hide')
-
-    if App.Config.get('smime_integration') == true
-      selection = App.UiElement.select.render(
-        name: "#{name}::sign"
-        multiple: false
-        options: {
-          'no': __('Do not sign email')
-          'discard': __('Sign email (if not possible, discard notification)')
-          'always': __('Sign email (if not possible, send notification anyway)')
-        }
-        value: meta.sign
-        translate: true
-      )
-
-      elementRow.find('.js-sign').html(selection)
-
-      selection = App.UiElement.select.render(
-        name: "#{name}::encryption"
-        multiple: false
-        options: {
-          'no': __('Do not encrypt email')
-          'discard': __('Encrypt email (if not possible, discard notification)')
-          'always': __('Encrypt email (if not possible, send notification anyway)')
-        }
-        value: meta.encryption
-        translate: true
-      )
-
-      elementRow.find('.js-encryption').html(selection)
-
-  @buildArticleArea: (articleType, elementFull, elementRow, groupAndAttribute, elements, meta, attribute) ->
-
-    return if elementRow.find(".js-setArticle .js-body-#{articleType}").get(0)
-
-    elementRow.find('.js-setArticle').empty()
-
-    name = "#{attribute.name}::article.#{articleType}"
-    selection = App.UiElement.select.render(
-      name: "#{name}::internal"
-      multiple: false
-      null: false
-      label: __('Visibility')
-      options: { true: 'internal', false: 'public' }
-      value: meta.internal
-      translate: true
-    )
-    articleElement = $( App.view('generic/ticket_perform_action/article')(
-      attribute: attribute
-      name: name
-      articleType: articleType
-      meta: meta || {}
-    ))
-    articleElement.find('.js-internal').html(selection)
-    articleElement.find('.js-body div[contenteditable="true"]').ce(
-      mode: 'richtext'
-      placeholder: __('message')
-      maxlength: 200000
-    )
-    new App.WidgetPlaceholder(
-      el: articleElement.find('.js-body div[contenteditable="true"]').parent()
-      objects: [
-        {
-          prefix: 'ticket'
-          object: 'Ticket'
-          display: __('Ticket')
-        },
-        {
-          prefix: 'article'
-          object: 'TicketArticle'
-          display: __('Article')
-        },
-        {
-          prefix: 'user'
-          object: 'User'
-          display: __('Current User')
-        },
-      ]
-    )
-
-    elementRow.find('.js-setArticle').html(articleElement).removeClass('hide')
+class App.UiElement.ticket_perform_action extends App.UiElement.ApplicationAction

+ 2 - 2
app/assets/javascripts/app/models/postmaster_filter.coffee

@@ -7,7 +7,7 @@ class App.PostmasterFilter extends App.Model
     { name: 'name',           display: __('Name'),              tag: 'input', type: 'text', limit: 250, 'null': false },
     { name: 'channel',        display: __('Channel'),           type: 'input', readonly: 1 },
     { name: 'match',          display: __('Match all of the following'),      tag: 'postmaster_match', note: __('You can use regular expression by using "regex:your_reg_exp".') },
-    { name: 'perform',        display: __('Perform action of the following'), tag: 'postmaster_set' },
+    { name: 'perform',        display: __('Perform action of the following'), tag: 'postmaster_set', user_action: false },
     { name: 'note',           display: __('Note'),              tag: 'textarea', limit: 250, null: true },
     { name: 'updated_at',     display: __('Updated'),           tag: 'datetime', readonly: 1 },
     { name: 'active',         display: __('Active'),            tag: 'active', default: true },
@@ -23,4 +23,4 @@ class App.PostmasterFilter extends App.Model
   @configure_clone = true
 
   @on 'create', (newRecord) ->
-    newRecord.channel = 'email'
+    newRecord.channel = 'email'

+ 5 - 0
app/models/channel/filter/database.rb

@@ -78,6 +78,11 @@ module Channel::Filter::Database
           next
         end
 
+        if meta['value'].present? && meta['operator'] == 'relative'
+          mail[ key.downcase.to_sym ] = TimeRangeHelper.relative(range: meta['range'], value: meta['value'])
+          next
+        end
+
         mail[ key.downcase.to_sym ] = meta['value']
       end
     end

+ 1 - 17
app/models/ticket.rb

@@ -958,23 +958,7 @@ perform changes on ticket
                     when 'static'
                       value['value']
                     when 'relative'
-                      pendtil = Time.zone.now
-                      val     = value['value'].to_i
-
-                      case value['range']
-                      when 'day'
-                        pendtil += val.days
-                      when 'minute'
-                        pendtil += val.minutes
-                      when 'hour'
-                        pendtil += val.hours
-                      when 'month'
-                        pendtil += val.months
-                      when 'year'
-                        pendtil += val.years
-                      end
-
-                      pendtil
+                      TimeRangeHelper.relative(range: value['range'], value: value['value'])
                     end
 
         if new_value

+ 43 - 44
i18n/zammad.pot

@@ -498,8 +498,8 @@ msgid "Accounts"
 msgstr ""
 
 #: app/assets/javascripts/app/controllers/_application_controller/table.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
 #: app/assets/javascripts/app/models/core_workflow.coffee
 #: app/assets/javascripts/app/views/api.jst.eco
 #: app/assets/javascripts/app/views/calendar/holiday_selector.jst.eco
@@ -785,7 +785,7 @@ msgstr ""
 msgid "All Tickets"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 msgid "All agents"
 msgstr ""
 
@@ -990,8 +990,8 @@ msgstr ""
 msgid "Are you sure?"
 msgstr ""
 
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
 msgid "Article"
 msgstr ""
 
@@ -2432,7 +2432,7 @@ msgstr ""
 msgid "Current Token"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/models/signature.coffee
 #: app/assets/javascripts/app/models/text_module.coffee
 msgid "Current User"
@@ -2476,10 +2476,9 @@ msgid "Custom address is not set"
 msgstr ""
 
 #: app/assets/javascripts/app/controllers/_application_controller/form.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
 #: app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee
-#: app/assets/javascripts/app/controllers/_ui_element/postmaster_set.coffee
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
 #: app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_customer.coffee
 #: app/assets/javascripts/app/controllers/ticket_customer.coffee
 #: app/assets/javascripts/app/controllers/ticket_zoom/sidebar_customer.coffee
@@ -3448,11 +3447,11 @@ msgstr ""
 msgid "Display name"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 msgid "Do not encrypt email"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 msgid "Do not sign email"
 msgstr ""
 
@@ -3646,7 +3645,7 @@ msgid "Elasticsearch needs to be configured!"
 msgstr ""
 
 #: app/assets/javascripts/app/controllers/_channel/email.coffee
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/controllers/agent_ticket_create.coffee
 #: app/assets/javascripts/app/controllers/getting_started/channel.coffee
 #: app/assets/javascripts/app/models/email_address.coffee
@@ -3906,11 +3905,11 @@ msgstr ""
 msgid "Encrypt"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 msgid "Encrypt email (if not possible, discard notification)"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 msgid "Encrypt email (if not possible, send notification anyway)"
 msgstr ""
 
@@ -4628,7 +4627,6 @@ msgstr ""
 
 #: app/assets/javascripts/app/controllers/_integration/slack.coffee
 #: app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee
-#: app/assets/javascripts/app/controllers/_ui_element/postmaster_set.coffee
 #: app/assets/javascripts/app/controllers/group.coffee
 #: app/assets/javascripts/app/models/ticket.coffee
 #: app/assets/javascripts/app/views/agent_ticket_view/detail.jst.eco
@@ -5334,7 +5332,6 @@ msgstr ""
 msgid "Intermediate"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/postmaster_set.coffee
 #: app/assets/javascripts/app/controllers/knowledge_base/content_can_be_published_form.coffee
 #: app/assets/javascripts/app/controllers/knowledge_base/content_controller.coffee
 msgid "Internal"
@@ -6559,7 +6556,7 @@ msgstr ""
 msgid "Next in overview"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 msgid "No"
 msgstr ""
 
@@ -6817,7 +6814,7 @@ msgstr ""
 msgid "Not configured"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/models/chat.coffee
 #: app/assets/javascripts/app/models/email_address.coffee
 #: app/assets/javascripts/app/models/group.coffee
@@ -6862,7 +6859,7 @@ msgstr ""
 msgid "Notice"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/views/mailer/application.html.erb
 msgid "Notification"
 msgstr ""
@@ -7188,8 +7185,7 @@ msgstr ""
 msgid "Overwrite Draft"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/postmaster_set.coffee
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/models/ticket.coffee
 #: app/assets/javascripts/app/views/agent_ticket_view/detail.jst.eco
 #: app/graphql/gql/types/overview_type.rb
@@ -7553,7 +7549,6 @@ msgid "Prio"
 msgstr ""
 
 #: app/assets/javascripts/app/controllers/_application_controller/form.coffee
-#: app/assets/javascripts/app/controllers/_ui_element/postmaster_set.coffee
 #: app/assets/javascripts/app/models/core_workflow.coffee
 #: app/assets/javascripts/app/models/ticket.coffee
 #: app/assets/javascripts/app/views/agent_ticket_view/detail.jst.eco
@@ -8046,7 +8041,7 @@ msgid "SLAs"
 msgstr ""
 
 #: app/assets/javascripts/app/controllers/_channel/sms.coffee
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 msgid "SMS"
 msgstr ""
 
@@ -8354,7 +8349,6 @@ msgstr ""
 msgid "Send to clients"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/postmaster_set.coffee
 #: app/assets/javascripts/app/models/ticket_article.coffee
 #: app/assets/javascripts/app/views/channel/sms_account_overview.jst.eco
 #: app/models/channel/driver/sms/massenversand.rb
@@ -8377,7 +8371,7 @@ msgstr ""
 msgid "Sender based on Reply-To header"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 msgid "Sender of last article"
 msgstr ""
 
@@ -8632,11 +8626,11 @@ msgstr ""
 msgid "Sign"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 msgid "Sign email (if not possible, discard notification)"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 msgid "Sign email (if not possible, send notification anyway)"
 msgstr ""
 
@@ -8833,7 +8827,6 @@ msgstr ""
 msgid "Starting with Zammad 5.1, translations can be contributed exclusively via \"translations.zammad.org\" %l."
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/postmaster_set.coffee
 #: app/assets/javascripts/app/models/chat_sessions.coffee
 #: app/assets/javascripts/app/models/data_privacy_task.coffee
 #: app/assets/javascripts/app/models/ticket.coffee
@@ -9031,10 +9024,6 @@ msgstr ""
 msgid "Tab has changed, do you really want to close it?"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/postmaster_set.coffee
-msgid "Tag"
-msgstr ""
-
 #: app/assets/javascripts/app/controllers/chat.coffee
 #: app/assets/javascripts/app/controllers/tag.coffee
 #: app/assets/javascripts/app/views/knowledge_base/_reader_tags.jst.eco
@@ -9913,9 +9902,10 @@ msgid "Thursday"
 msgstr ""
 
 #: app/assets/javascripts/app/controllers/_manage/ticket.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
 #: app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/postmaster_set.coffee
 #: app/assets/javascripts/app/controllers/ticket_zoom/sidebar_ticket.coffee
 #: app/assets/javascripts/app/models/signature.coffee
 #: app/assets/javascripts/app/models/text_module.coffee
@@ -10364,7 +10354,6 @@ msgstr ""
 
 #: app/assets/javascripts/app/controllers/_channel/email.coffee
 #: app/assets/javascripts/app/controllers/_ui_element/object_manager_attribute.coffee
-#: app/assets/javascripts/app/controllers/_ui_element/postmaster_set.coffee
 #: app/assets/javascripts/app/controllers/getting_started/channel_email.coffee
 #: app/assets/javascripts/app/controllers/widget/ticket_bulk_form.coffee
 #: app/assets/javascripts/app/models/ticket_article.coffee
@@ -10638,8 +10627,8 @@ msgid "Used when viewing a Ticket"
 msgstr ""
 
 #: app/assets/javascripts/app/controllers/_channel/email.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
 #: app/assets/javascripts/app/controllers/agent_ticket_create/sidebar_customer.coffee
 #: app/assets/javascripts/app/controllers/cti.coffee
 #: app/assets/javascripts/app/controllers/getting_started/channel_email.coffee
@@ -10770,7 +10759,7 @@ msgstr ""
 msgid "Value"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 msgid "Variables"
 msgstr ""
 
@@ -10874,7 +10863,7 @@ msgstr ""
 msgid "Viewing ticket"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/controllers/knowledge_base/content_can_be_published_dialog.coffee
 #: app/assets/javascripts/app/controllers/widget/ticket_bulk_form.coffee
 #: app/assets/javascripts/app/models/ticket_article.coffee
@@ -10965,7 +10954,7 @@ msgstr ""
 
 #: app/assets/javascripts/app/controllers/_channel/sms.coffee
 #: app/assets/javascripts/app/controllers/_integration/slack.coffee
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/controllers/webhook.coffee
 #: app/assets/javascripts/app/views/channel/sms_account_overview.jst.eco
 msgid "Webhook"
@@ -11189,7 +11178,7 @@ msgid "Yellow"
 msgstr ""
 
 #: app/assets/javascripts/app/controllers/_application_controller/_modal_generic_confirm.coffee
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/controllers/tag.coffee
 #: app/assets/javascripts/app/views/ticket_shared_draft_modal.coffee
 msgid "Yes"
@@ -11536,6 +11525,7 @@ msgstr ""
 msgid "after (absolute)"
 msgstr ""
 
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
 msgid "after (relative)"
 msgstr ""
@@ -11582,6 +11572,7 @@ msgstr ""
 msgid "before (absolute)"
 msgstr ""
 
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
 msgid "before (relative)"
 msgstr ""
@@ -11666,13 +11657,13 @@ msgstr ""
 msgid "created %s"
 msgstr ""
 
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
 msgid "current user"
 msgstr ""
 
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
 msgid "current user organization"
 msgstr ""
 
@@ -11829,6 +11820,7 @@ msgstr ""
 msgid "from"
 msgstr ""
 
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
 msgid "from (relative)"
 msgstr ""
@@ -11946,7 +11938,7 @@ msgstr ""
 msgid "inactive"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/controllers/knowledge_base/content_can_be_published_form.coffee
 msgid "internal"
 msgstr ""
@@ -12033,7 +12025,7 @@ msgstr ""
 msgid "merged"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 msgid "message"
 msgstr ""
 
@@ -12153,7 +12145,7 @@ msgstr ""
 msgid "phone"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 msgid "public"
 msgstr ""
 
@@ -12173,6 +12165,10 @@ msgstr ""
 msgid "regex mismatch"
 msgstr ""
 
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
+msgid "relative"
+msgstr ""
+
 #: db/seeds/settings.rb
 msgid "relative - e. g. \"2 hours ago\" or \"2 days and 15 minutes ago\""
 msgstr ""
@@ -12293,13 +12289,13 @@ msgstr ""
 msgid "sms"
 msgstr ""
 
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
 msgid "specific organization"
 msgstr ""
 
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
 msgid "specific user"
 msgstr ""
 
@@ -12341,6 +12337,7 @@ msgstr ""
 msgid "ticket.customer"
 msgstr ""
 
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
 msgid "till (relative)"
 msgstr ""
@@ -12371,7 +12368,7 @@ msgstr ""
 msgid "twitter status"
 msgstr ""
 
-#: app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 msgid "unassign user"
 msgstr ""
 
@@ -12409,10 +12406,12 @@ msgstr ""
 msgid "will be deleted"
 msgstr ""
 
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
 msgid "within last (relative)"
 msgstr ""
 
+#: app/assets/javascripts/app/controllers/_ui_element/_application_action.coffee
 #: app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee
 msgid "within next (relative)"
 msgstr ""

+ 22 - 0
lib/time_range_helper.rb

@@ -0,0 +1,22 @@
+# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+class TimeRangeHelper
+  def self.relative(from: Time.zone.now, range: 'day', value: 1)
+    value = value.to_i
+
+    case range
+    when 'day'
+      from += value.days
+    when 'minute'
+      from += value.minutes
+    when 'hour'
+      from += value.hours
+    when 'month'
+      from += value.months
+    when 'year'
+      from += value.years
+    end
+
+    from
+  end
+end

+ 4 - 4
public/assets/tests/qunit/form.js

@@ -828,7 +828,7 @@ QUnit.test("form postmaster filter", assert => {
         { name: 'input1', display: 'Input1', tag: 'input', type: 'text', limit: 100, null: true, default: 'some not used default' },
         { name: 'input2', display: 'Input2', tag: 'input', type: 'text', limit: 100, null: true, default: 'some used default' },
         { name: 'match',  display: 'Match',  tag: 'postmaster_match', null: false, default: false},
-        { name: 'set',    display: 'Set',    tag: 'postmaster_set', null: false, default: false},
+        { name: 'set',    display: 'Set',    tag: 'postmaster_set', null: false, default: false, user_action: false},
       ],
     },
     params: defaults,
@@ -927,13 +927,13 @@ QUnit.test("form postmaster filter", assert => {
       'x-zammad-ticket-group_id': {
         value: '1'
       },
-      'x-zammad-ticket-priority_id': {
-        value: '1'
-      },
       'x-zammad-ticket-tags': {
         operator: 'add',
         value: 'test, test1'
       },
+      'x-zammad-ticket-title': {
+        value: ''
+      },
     },
   };
   assert.deepEqual(params, test_params, 'form param check 12')

+ 46 - 0
spec/models/channel/filter/database_spec.rb

@@ -0,0 +1,46 @@
+# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
+
+require 'rails_helper'
+
+RSpec.describe Channel::Filter::Database, type: :channel_filter do
+  let(:mail_hash) { Channel::EmailParser.new.parse(<<~RAW.chomp) }
+    From: daffy.duck@acme.corp
+    To: batman@marvell.com
+    Subject: Anvil
+
+    I can haz anvil!
+  RAW
+
+  describe 'Cannot set date for pending close status in postmaster filter #4206', db_strategy: :reset do
+    before do
+      freeze_time
+
+      create :object_manager_attribute_date, name: '4206_date'
+      create :object_manager_attribute_datetime, name: '4206_datetime'
+      create :postmaster_filter, perform: {
+        'x-zammad-ticket-pending_time'  => { 'operator' => 'relative', 'value' => '12', 'range' => 'minute' },
+        'x-zammad-ticket-state_id'      => { 'value' => Ticket::State.find_by(name: 'pending reminder').id },
+        'x-zammad-ticket-4206_datetime' => { 'operator' => 'static', 'value' => '2022-08-18T06:00:00.000Z' },
+        'x-zammad-ticket-4206_date'     => { 'value' => '2022-08-19' }
+      }
+      ObjectManager::Attribute.migration_execute
+      filter(mail_hash)
+    end
+
+    it 'does set values for pending time' do
+      expect(mail_hash['x-zammad-ticket-pending_time']).to eq(12.minutes.from_now)
+    end
+
+    it 'does set values for state_id' do
+      expect(mail_hash['x-zammad-ticket-state_id']).to eq(Ticket::State.find_by(name: 'pending reminder').id)
+    end
+
+    it 'does set values for 4206_datetime' do
+      expect(mail_hash['x-zammad-ticket-4206_datetime']).to eq('2022-08-18T06:00:00.000Z')
+    end
+
+    it 'does set values for 4206_date' do
+      expect(mail_hash['x-zammad-ticket-4206_date']).to eq('2022-08-19')
+    end
+  end
+end