Browse Source

improve email filters by adding a tag option - closes #1991

Muhammad Nuzaihan 6 years ago
parent
commit
9f559276e4

+ 2 - 2
Gemfile.lock

@@ -306,7 +306,7 @@ GEM
       slop (~> 3.0)
     public_suffix (3.0.1)
     puma (3.11.0)
-    rack (2.0.4)
+    rack (2.0.5)
     rack-livereload (0.3.16)
       rack
     rack-test (1.0.0)
@@ -405,7 +405,7 @@ GEM
       simplecov (>= 0.4.1)
     slack-notifier (2.3.1)
     slop (3.6.0)
-    sprockets (3.7.1)
+    sprockets (3.7.2)
       concurrent-ruby (~> 1.0)
       rack (> 1, < 3)
     sprockets-rails (3.2.1)

+ 53 - 2
app/assets/javascripts/app/controllers/_ui_element/postmaster_set.coffee

@@ -4,6 +4,7 @@ class App.UiElement.postmaster_set
     groups =
       ticket:
         name: 'Ticket'
+        model: 'Ticket'
         options: [
           {
             value:    'priority_id'
@@ -15,6 +16,11 @@ class App.UiElement.postmaster_set
             name:     'State'
             relation: 'TicketState'
           }
+          {
+            value:    'tags'
+            name:     'Tag'
+            tag:      'tag'
+          }
           {
             value:    'customer_id'
             name:     'Customer'
@@ -64,6 +70,24 @@ class App.UiElement.postmaster_set
           }
         ]
 
+    elements = {}
+    for groupKey, groupMeta of groups
+      if !App[groupMeta.model]
+        elements["#{groupKey}.email"] = { name: 'email', display: 'Email' }
+      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)
+              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
@@ -91,11 +115,11 @@ class App.UiElement.postmaster_set
     for item in groups.ticket.options
       item.value = "x-zammad-ticket-#{item.value}"
 
-    groups
+    [elements, groups]
 
   @render: (attribute, params = {}) ->
 
-    groups = @defaults()
+    [elements, groups] = @defaults()
 
     selector = @buildAttributeSelector(groups, attribute)
 
@@ -121,7 +145,9 @@ class App.UiElement.postmaster_set
     item.find('.js-attributeSelector select').bind('change', (e) =>
       key = $(e.target).find('option:selected').attr('value')
       elementRow = $(e.target).closest('.js-filterElement')
+      groupAndAttribute = elementRow.find('.js-attributeSelector option:selected').attr('value')
       @rebuildAttributeSelectors(item, elementRow, key, attribute)
+      @buildOperator(item, elementRow, groupAndAttribute, elements, {},  attribute)
       @buildValue(item, elementRow, key, groups, undefined, undefined, attribute)
     )
 
@@ -203,3 +229,28 @@ class App.UiElement.postmaster_set
     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')
+
+    if !meta.operator
+      meta.operator = currentOperator
+
+    name = "#{attribute.name}::#{groupAndAttribute}::operator"
+
+    selection = $("<select class=\"form-control\" name=\"#{name}\"></select>")
+    attributeConfig = elements[groupAndAttribute]
+
+    if !attributeConfig.operator
+      elementRow.find('.js-operator').addClass('hide')
+    else
+      elementRow.find('.js-operator').removeClass('hide')
+    if 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)

+ 5 - 0
app/assets/javascripts/app/controllers/object_manager.coffee

@@ -143,12 +143,17 @@ class Items extends App.ControllerSubContent
     e.preventDefault()
     id   = $(e.target).closest('tr').data('id')
     item = App.ObjectManagerAttribute.find(id)
+    ui = @
     @ajax(
       id:    "object_manager_attributes/#{id}"
       type:  'DELETE'
       url:   "#{@apiPath}/object_manager_attributes/#{id}"
       success: (data) =>
         @render()
+      error: (jqXHR, textStatus, errorThrown) ->
+        ui.log 'errors'
+        # this code is unreachable so alert will do fine
+        alert(jqXHR.responseJSON.error)
     )
 
   discard: (e) ->

+ 7 - 1
app/assets/javascripts/app/views/generic/postmaster_set.jst.eco

@@ -6,6 +6,12 @@
           <%- @Icon('arrow-down', 'dropdown-arrow') %>
         </div>
       </div>
+      <div class="controls">
+        <div class="u-positionOrigin js-operator">
+          <select></select>
+          <%- @Icon('arrow-down') %>
+        </div>
+      </div>
       <div class="controls js-value"></div>
     </div>
     <div class="filter-controls">
@@ -17,4 +23,4 @@
       </div>
     </div>
   </div>
-</div>
+</div>

+ 6 - 2
app/assets/javascripts/app/views/object_manager/index.jst.eco

@@ -71,8 +71,12 @@
             <%- @T('will be deleted') %>
           <% else if item.to_migrate is true || item.to_config is true: %>
             <%- @T('has changed') %>
-          <% else if item.editable isnt false: %>
-            <a href="#" class="js-delete" title="<%- @Ti('Delete') %>"><%- @Icon('trash') %></a>
+          <% else if item.editable: %>
+            <% if item.deletable: %>
+              <a href="#" class="js-delete" title="<%- @Ti('Delete') %>"><%- @Icon('trash') %></a>
+            <% else: %>
+              <span class="is-disabled" title="<%= item.not_deletable_reason %>"><%- @Icon('trash') %></span>
+            <% end %>
           <% end %>
         </td>
       </tr>

+ 5 - 0
app/assets/stylesheets/zammad.scss

@@ -9610,3 +9610,8 @@ body.fit {
 .flex-spacer {
   flex: 1;
 }
+
+span.is-disabled {
+  cursor: not-allowed;
+  opacity: 0.5;
+}

+ 3 - 0
app/controllers/object_manager_attributes_controller.rb

@@ -77,6 +77,9 @@ class ObjectManagerAttributesController < ApplicationController
       name: object_manager_attribute.name,
     )
     model_destroy_render_item
+  rescue => e
+    logger.error e
+    raise Exceptions::UnprocessableEntity, e
   end
 
   # POST /object_manager_attributes_discard_changes

+ 10 - 0
app/models/channel/email_parser.rb

@@ -238,6 +238,14 @@ returns
 
         # create ticket
         ticket.save!
+
+      end
+
+      # apply tags to ticket
+      if mail['x-zammad-ticket-tags'.to_sym].present?
+        mail['x-zammad-ticket-tags'.to_sym].each do |tag|
+          ticket.tag_add(tag)
+        end
       end
 
       # set attributes
@@ -309,6 +317,8 @@ returns
   def self.check_attributes_by_x_headers(header_name, value)
     class_name = nil
     attribute = nil
+    # skip check attributes if it is tags
+    return true if header_name == 'x-zammad-ticket-tags'
     if header_name =~ /^x-zammad-(.+?)-(followup-|)(.*)$/i
       class_name = $1
       attribute = $3

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

@@ -45,6 +45,29 @@ module Channel::Filter::Database
       filter[:perform].each do |key, meta|
         next if !Channel::EmailParser.check_attributes_by_x_headers(key, meta['value'])
         Rails.logger.info "  perform '#{key.downcase}' = '#{meta.inspect}'"
+
+        if key.downcase == 'x-zammad-ticket-tags' && meta['value'].present? && meta['operator'].present?
+          mail[ 'x-zammad-ticket-tags'.downcase.to_sym ] ||= []
+          tags = meta['value'].split(',')
+
+          case meta['operator']
+          when 'add'
+            tags.each do |tag|
+              next if tag.blank?
+              tag.strip!
+              next if mail[ 'x-zammad-ticket-tags'.downcase.to_sym ].include?(tag)
+              mail[ 'x-zammad-ticket-tags'.downcase.to_sym ].push tag
+            end
+          when 'remove'
+            tags.each do |tag|
+              next if tag.blank?
+              tag.strip!
+              mail[ 'x-zammad-ticket-tags'.downcase.to_sym ] -= [tag]
+            end
+          end
+          next
+        end
+
         mail[ key.downcase.to_sym ] = meta['value']
       end
     end

+ 126 - 0
app/models/object_manager/attribute.rb

@@ -29,12 +29,25 @@ list of all attributes
 
   def self.list_full
     result = ObjectManager::Attribute.all.order('position ASC, name ASC')
+    references = ObjectManager::Attribute.attribute_to_references_hash
     attributes = []
     assets = {}
     result.each do |item|
       attribute = item.attributes
       attribute[:object] = ObjectLookup.by_id(item.object_lookup_id)
       attribute.delete('object_lookup_id')
+
+      # an attribute is deletable if it is both editable and not referenced by other Objects (Triggers, Overviews, Schedulers)
+      deletable = true
+      not_deletable_reason = ''
+      if ObjectManager::Attribute.attribute_used_by_references?(attribute[:object], attribute['name'], references)
+        deletable = false
+        not_deletable_reason = ObjectManager::Attribute.attribute_used_by_references_humaniced(attribute[:object], attribute['name'], references)
+      end
+      attribute[:deletable] = attribute['editable'] && deletable == true
+      if not_deletable_reason.present?
+        attribute[:not_deletable_reason] = "This attribute is referenced by #{not_deletable_reason} and thus cannot be deleted!"
+      end
       attributes.push attribute
     end
     attributes
@@ -354,6 +367,10 @@ use "force: true" to delete also not editable fields
     # lookups
     if data[:object]
       data[:object_lookup_id] = ObjectLookup.by_name(data[:object])
+    elsif data[:object_lookup_id]
+      data[:object] = ObjectLookup.by_id(data[:object_lookup_id])
+    else
+      raise 'ERROR: need object or object_lookup_id param!'
     end
 
     data[:name].downcase!
@@ -371,6 +388,12 @@ use "force: true" to delete also not editable fields
       raise "ERROR: #{data[:object]}.#{data[:name]} can't be removed!"
     end
 
+    # check to make sure that no triggers, overviews, or schedulers references this attribute
+    if ObjectManager::Attribute.attribute_used_by_references?(data[:object], data[:name])
+      text = ObjectManager::Attribute.attribute_used_by_references_humaniced(data[:object], data[:name])
+      raise "ERROR: #{data[:object]}.#{data[:name]} is referenced by #{text} and thus cannot be deleted!"
+    end
+
     # if record is to create, just destroy it
     if record.to_create
       record.destroy
@@ -721,6 +744,109 @@ to send no browser reload event, pass false
     true
   end
 
+=begin
+
+where attributes are used by triggers, overviews or schedulers
+
+  result = ObjectManager::Attribute.attribute_to_references_hash
+
+  result = {
+    ticket.category: {
+      Trigger: ['abc', 'xyz'],
+      Overview: ['abc1', 'abc2'],
+    },
+    ticket.field_b: {
+      Trigger: ['abc'],
+      Overview: ['abc1', 'abc2'],
+    },
+  },
+
+=end
+
+  def self.attribute_to_references_hash
+    objects = Trigger.select(:name, :condition) + Overview.select(:name, :condition) + Job.select(:name, :condition)
+    attribute_list = {}
+    objects.each do |item|
+      item.condition.each do |condition_key, _condition_attributes|
+        attribute_list[condition_key] ||= {}
+        attribute_list[condition_key][item.class.name] ||= []
+        next if attribute_list[condition_key][item.class.name].include?(item.name)
+        attribute_list[condition_key][item.class.name].push item.name
+      end
+    end
+    attribute_list
+  end
+
+=begin
+
+is certain attribute used by triggers, overviews or schedulers
+
+  ObjectManager::Attribute.attribute_used_by_references?('Ticket', 'attribute_name')
+
+=end
+
+  def self.attribute_used_by_references?(object_name, attribute_name, references = attribute_to_references_hash)
+    references.each do |reference_key, _relations|
+      local_object, local_attribute = reference_key.split('.')
+      next if local_object != object_name.downcase
+      next if local_attribute != attribute_name
+      return true
+    end
+    false
+  end
+
+=begin
+
+is certain attribute used by triggers, overviews or schedulers
+
+  result = ObjectManager::Attribute.attribute_used_by_references('Ticket', 'attribute_name')
+
+  result = {
+    Trigger: ['abc', 'xyz'],
+    Overview: ['abc1', 'abc2'],
+  }
+
+=end
+
+  def self.attribute_used_by_references(object_name, attribute_name, references = attribute_to_references_hash)
+    result = {}
+    references.each do |reference_key, relations|
+      local_object, local_attribute = reference_key.split('.')
+      next if local_object != object_name.downcase
+      next if local_attribute != attribute_name
+      relations.each do |relation, relation_names|
+        result[relation] ||= []
+        result[relation].push relation_names.sort
+      end
+      break
+    end
+    result
+  end
+
+=begin
+
+is certain attribute used by triggers, overviews or schedulers
+
+  text = ObjectManager::Attribute.attribute_used_by_references_humaniced('Ticket', 'attribute_name', references)
+
+=end
+
+  def self.attribute_used_by_references_humaniced(object_name, attribute_name, references = nil)
+    result = if references.present?
+               ObjectManager::Attribute.attribute_used_by_references(object_name, attribute_name, references)
+             else
+               ObjectManager::Attribute.attribute_used_by_references(object_name, attribute_name)
+             end
+    not_deletable_reason = ''
+    result.each do |relation, relation_names|
+      if not_deletable_reason.present?
+        not_deletable_reason += '; '
+      end
+      not_deletable_reason += "#{relation}: #{relation_names.sort.join(',')}"
+    end
+    not_deletable_reason
+  end
+
   def self.reset_database_info(model)
     model.connection.schema_cache.clear!
     model.reset_column_information

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