Browse Source

Fixes #4702 - Add regex as operator for ticket filter in triggers.

Co-authored-by: Martin Gruner <mg@zammad.com>
Co-authored-by: Florian Liebe <fl@zammad.com>
Martin Gruner 1 year ago
parent
commit
b59ae749da

+ 1 - 1
app/assets/javascripts/app/controllers/_manage/ticket_auto_assignment.coffee

@@ -22,7 +22,7 @@ class App.SettingTicketAutoAssignment extends App.ControllerSubContent
     @html(App.view('settings/ticket_auto_assignment')())
 
     configure_attributes = [
-      { name: 'condition', display: __('Conditions for affected objects'), tag: 'ticket_selector', null: false, preview: false, action: false, hasChanged: false, article: false },
+      { name: 'condition', display: __('Conditions for affected objects'), tag: 'ticket_selector', null: false, preview: false, action: false, hasChanged: false, article: false, hasRegexOperators: true },
     ]
 
     ticket_auto_assignment_selector = App.Setting.get('ticket_auto_assignment_selector')

+ 3 - 0
app/assets/javascripts/app/controllers/_ui_element/_application_selector.coffee

@@ -54,6 +54,9 @@ class App.UiElement.ApplicationSelector
         '^textarea$': [__('contains'), __('contains not'), __('has changed')]
         '^tag$': [__('contains all'), __('contains one'), __('contains all not'), __('contains one not')]
 
+    if attribute.hasRegexOperators && App.Config.get('ticket_conditions_allow_regular_expression_operators')
+      operators_type['^input$'].push(__('matches regex'), __('does not match regex'))
+
     operators_name =
       '_id$': [__('is'), __('is not')]
       '_ids$': [__('is'), __('is not')]

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

@@ -72,7 +72,7 @@ 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'), __('regex match'), __('regex mismatch'), __('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')]
       '^tag$': [__('contains all'), __('contains one'), __('contains all not'), __('contains one not')]
 
     operatorsName =

+ 1 - 1
app/assets/javascripts/app/models/trigger.coffee

@@ -6,7 +6,7 @@ class App.Trigger extends App.Model
     { name: 'name',                     display: __('Name'),             tag: 'input',     type: 'text', limit: 100,  null: false },
     { name: 'activator',                display: __('Activated by'),     tag: 'select',    type: 'text', limit: 50,   null: true, options: { action: __('Action'), time: __('Time event') }, note: __('Triggers activated by actions are executed whenever a ticket is created or updated, while triggers activated by time events are executed when certain times are reached (e.g. pending time, escalation).') },
     { name: 'execution_condition_mode', display: __('Action execution'), tag: 'radio',     type: 'text', limit: 50,   null: true, options: [ { value: 'selective', name: __('Selective (default)'), note: __('When at least one field from conditions was updated or article was added and conditions match') }, { value: 'always', name: __('Always'), note: __('When conditions match') } ] },
-    { name: 'condition',                display: __('Conditions for affected objects'), tag: 'ticket_selector',       null: false, preview: false, action: true, hasChanged: true, executionTime: true, hasReached: true },
+    { name: 'condition',                display: __('Conditions for affected objects'), tag: 'ticket_selector',       null: false, preview: false, action: true, hasChanged: true, executionTime: true, hasReached: true, hasRegexOperators: true },
     { name: 'perform',                  display: __('Execute changes on objects'),      tag: 'ticket_perform_action', null: true, notification: true, trigger: true },
     { name: 'note',                     display: __('Note'),             tag: 'textarea',                limit: 250,  null: true },
     { name: 'active',                   display: __('Active'),           tag: 'active',    default: true },

+ 1 - 0
app/frontend/shared/types/config.ts

@@ -61,6 +61,7 @@ export interface ConfigList {
   ticket_auto_assignment?: boolean | null
   ticket_auto_assignment_selector: unknown
   ticket_auto_assignment_user_ids_ignore: unknown
+  ticket_conditions_allow_regular_expression_operators?: boolean | null
   ticket_duplicate_detection?: boolean | null
   ticket_duplicate_detection_attributes: unknown
   ticket_duplicate_detection_body: string

+ 1 - 1
app/models/core_workflow/condition/regex_mismatch.rb → app/models/core_workflow/condition/does_not_match_regex.rb

@@ -1,6 +1,6 @@
 # Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
 
-class CoreWorkflow::Condition::RegexMismatch < CoreWorkflow::Condition::Backend
+class CoreWorkflow::Condition::DoesNotMatchRegex < CoreWorkflow::Condition::Backend
   def match
     return true if value.blank?
 

+ 1 - 1
app/models/core_workflow/condition/regex_match.rb → app/models/core_workflow/condition/matches_regex.rb

@@ -1,6 +1,6 @@
 # Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
 
-class CoreWorkflow::Condition::RegexMatch < CoreWorkflow::Condition::Backend
+class CoreWorkflow::Condition::MatchesRegex < CoreWorkflow::Condition::Backend
   def match
     result = false
     value.each do |current_value|

+ 0 - 9
app/models/ticket/selector/base.rb

@@ -106,15 +106,6 @@ class Ticket::Selector::Base
     end
   end
 
-  def valid?
-    ticket_count, _tickets = self.class.new(selector: selector, options: options.merge(limit: 1, execution_time: true, ticket_id: 1)).get
-    return if ticket_count.nil?
-
-    true
-  rescue
-    nil
-  end
-
   def attribute_exists?(attribute, check_condition = @selector[:conditions])
     result = false
     check_condition.each do |condition|

+ 16 - 1
app/models/ticket/selector/sql.rb

@@ -16,6 +16,8 @@ class Ticket::Selector::Sql < Ticket::Selector::Base
     'is not in working time',
     'starts with',
     'ends with',
+    'matches regex',
+    'does not match regex',
   ].freeze
 
   attr_accessor :final_query, :final_bind_params, :final_tables, :changed_attributes
@@ -313,6 +315,12 @@ class Ticket::Selector::Sql < Ticket::Selector::Base
     elsif block_condition[:operator] == 'contains not'
       query << "#{attribute} NOT #{like} (?)"
       bind_params.push "%#{block_condition[:value]}%"
+    elsif block_condition[:operator] == 'matches regex'
+      query << sql_helper.regex_match(attribute, negated: false)
+      bind_params.push block_condition[:value]
+    elsif block_condition[:operator] == 'does not match regex'
+      query << sql_helper.regex_match(attribute, negated: true)
+      bind_params.push block_condition[:value]
     elsif block_condition[:operator] == 'contains all'
       if attribute_table == 'ticket' && attribute_name == 'tags'
         query << "? = (
@@ -444,7 +452,7 @@ class Ticket::Selector::Sql < Ticket::Selector::Base
 
     return true if self.class.valid_operator? condition[:operator]
 
-    raise "Invalid condition, operator #{condition[:operator]} is invalid #{condition.inspect}"
+    raise "Invalid condition, operator '#{condition[:operator]}' is invalid #{condition.inspect}"
   end
 
   def time_based_trigger?(condition, warning:)
@@ -489,4 +497,11 @@ class Ticket::Selector::Sql < Ticket::Selector::Base
   def self.valid_operator?(operator)
     VALID_OPERATORS.any? { |elem| operator.match? elem }
   end
+
+  def valid?
+    ticket_count, _tickets = Ticket.selectors(selector, **options.merge(limit: 1, execution_time: true, ticket_id: 1, access: 'ignore'))
+    !ticket_count.nil?
+  rescue
+    false
+  end
 end

+ 35 - 0
db/migrate/20230705074628_ticket_conditions_regular_expression_operators.rb

@@ -0,0 +1,35 @@
+# Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
+
+class TicketConditionsRegularExpressionOperators < ActiveRecord::Migration[6.1]
+  def change
+    # return if it's a new setup
+    return if !Setting.exists?(name: 'system_init_done')
+
+    Setting.create_if_not_exists(
+      title:       'Ticket Conditions Regular Expression Operators',
+      name:        'ticket_conditions_allow_regular_expression_operators',
+      area:        'Ticket::Core',
+      description: 'Defines if the ticket conditions editor supports regular expression operators for triggers and ticket auto assignment.',
+      options:     {
+        form: [
+          {
+            display: '',
+            null:    true,
+            name:    'ticket_conditions_allow_regular_expression_operators',
+            tag:     'boolean',
+            options: {
+              true  => 'yes',
+              false => 'no',
+            },
+          },
+        ],
+      },
+      state:       true,
+      preferences: {
+        online_service_disable: true,
+        permission:             ['admin.ticket'],
+      },
+      frontend:    true
+    )
+  end
+end

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