Browse Source

Fixes #4862 and #4892 - Possibility to add custom ticket states and priorities.

Co-authored-by: Tobias Schäfer <ts@zammad.com>
Co-authored-by: Dusan Vuckovic <dv@zammad.com>
Co-authored-by: Martin Gruner <mg@zammad.com>
Co-authored-by: Mantas Masalskis <mm@zammad.com>
Co-authored-by: Dominik Klein <dk@zammad.com>
Tobias Schäfer 1 year ago
parent
commit
9829526f4a

+ 57 - 0
app/assets/javascripts/app/controllers/ticket_priority.coffee

@@ -0,0 +1,57 @@
+class TicketPriority extends App.ControllerSubContent
+  @requiredPermission: 'admin.object'
+  header: __('Ticket Priority')
+  constructor: ->
+    super
+
+    @genericController = new App.ControllerGenericIndex(
+      el: @el
+      id: @id
+      genericObject: 'TicketPriority'
+      defaultSortBy: 'name'
+      handlers: [@formHandler]
+      pageData:
+        home:      'ticket_priorities'
+        object:    __('Ticket Priority')
+        objects:   __('Ticket Priorities')
+        navupdate: '#ticket_priorities'
+        buttons: [
+          { name: __('New Priority'), 'data-type': 'new', class: 'btn--success' }
+        ]
+        tableExtend: {
+          customActions: [
+            {
+              name: 'set_default_create'
+              display: __('Set default for new tickets')
+              icon: 'reload'
+              class: 'js-setDefaultCreate'
+              callback: (id) =>
+                @setDefaultPriority(id)
+              available: (object) ->
+                object.active and not object.default_create
+            }
+          ]
+        }
+      container: @el.closest('.content')
+    )
+
+  formHandler: (params, attribute, attributes, classname, form, ui) ->
+    form.find('[data-attribute-name="ui_icon"]').show()
+
+    return if App.Config.get('ui_ticket_overview_priority_icon') and form.find('[name="ui_color"]').val()
+
+    # Hide the UI icon selection in case:
+    #   - `ui_ticket_overview_priority_icon` setting is disabled
+    #   - `ui_color` form field is not set
+    form.find('[data-attribute-name="ui_icon"]').hide()
+
+  setDefaultPriority: (id) ->
+    currentItem = App.TicketPriority.findByAttribute('default_create', true)
+    selectedItem = App.TicketPriority.find(id)
+
+    return if currentItem.id is selectedItem.id
+
+    selectedItem.updateAttribute('default_create', true)
+    currentItem?.refresh(default_create: false)
+
+App.Config.set('TicketPriority', { prio: 3325, name: __('Ticket Priorities'), parent: '#manage', target: '#manage/ticket_priorities', controller: TicketPriority, permission: ['admin.object'], hidden: true }, 'NavBarAdmin')

+ 71 - 0
app/assets/javascripts/app/controllers/ticket_state.coffee

@@ -0,0 +1,71 @@
+class TicketState extends App.ControllerSubContent
+  @requiredPermission: 'admin.object'
+  header: __('Ticket States')
+  constructor: ->
+    super
+
+    @pendingActionStateTypeId = App.TicketStateType.findByAttribute('name', 'pending action').id.toString()
+
+    @genericController = new App.ControllerGenericIndex(
+      el: @el
+      id: @id
+      genericObject: 'TicketState'
+      defaultSortBy: 'name'
+      handlers: [@formHandler]
+      pageData:
+        home: 'ticket_states'
+        object: __('Ticket State')
+        objects: __('Ticket States')
+        navupdate: '#ticket_states'
+        buttons: [
+          { name: __('New Ticket State'), 'data-type': 'new', class: 'btn--success' }
+        ]
+        tableExtend: {
+          customActions: [
+            {
+              name: 'set_default_create'
+              display: __('Set default for new tickets')
+              icon: 'reload'
+              class: 'set_default_create'
+              callback: (id) =>
+                @setDefaultState('default_create', id)
+              available: (object) ->
+                object.active and not object.default_create
+            },
+            {
+              name: 'set_default_follow_up'
+              display: __('Set default for follow-ups')
+              icon: 'reload'
+              class: 'set_default_follow_up'
+              callback: (id) =>
+                @setDefaultState('default_follow_up', id)
+              available: (object) ->
+                object.active and not object.default_follow_up
+            }
+          ]
+        }
+      container: @el.closest('.content')
+      veryLarge: true
+    )
+
+  formHandler: (params, attribute, attributes, classname, form, ui) =>
+    if params.state_type_id is @pendingActionStateTypeId
+      form.find('[data-attribute-name="next_state_id"]').show()
+    else
+      form.find('[data-attribute-name="next_state_id"]').hide()
+
+  setDefaultState: (type, id) ->
+    currentItem = App.TicketState.findByAttribute(type, true)
+    selectedItem = App.TicketState.find(id)
+
+    return if currentItem.id is selectedItem.id
+
+    selectedItem.updateAttribute(type, true)
+    if type == 'default_create'
+      currentItem?.refresh(default_create: false)
+    else if type == 'default_follow_up'
+      currentItem?.refresh(default_follow_up: false)
+    else
+      console.error('Unknown default state type', type)
+
+App.Config.set('Ticket States', { prio: 3325, name: __('Ticket States'), parent: '#manage', target: '#manage/ticket_states', controller: TicketState, permission: ['admin.object'], hidden: true }, 'NavBarAdmin')

+ 11 - 0
app/assets/javascripts/app/models/object_manager_attribute.coffee

@@ -38,3 +38,14 @@ class App.ObjectManagerAttribute extends App.Model
         result[object].push(_.clone(row))
 
     result
+
+  manageRoute: ->
+    return if @.object isnt 'Ticket'
+
+    switch @.name
+      when 'priority_id'
+        return '#system/ticket_priorities'
+      when 'state_id'
+        return '#system/ticket_states'
+      else
+        return false

+ 23 - 5
app/assets/javascripts/app/models/ticket_priority.coffee

@@ -1,14 +1,32 @@
 class App.TicketPriority extends App.Model
-  @configure 'TicketPriority', 'name', 'note', 'active', 'updated_at'
+  @configure 'TicketPriority', 'name', 'default_create', 'ui_icon', 'ui_color', 'note', 'active', 'updated_at'
   @extend Spine.Model.Ajax
   @url: @apiPath + '/ticket_priorities'
   @configure_attributes = [
-    { name: 'name',       display: __('Name'),    tag: 'input',     type: 'text', limit: 100, null: false, translate: true },
-    { name: 'active',     display: __('Active'),  tag: 'active',    default: true },
-    { name: 'updated_at', display: __('Updated'), tag: 'datetime',  readonly: 1 },
-    { name: 'created_at', display: __('Created'), tag: 'datetime',  readonly: 1 },
+    { name: 'name',       display: __('Name'),     tag: 'input',    type: 'text', limit: 250, null: false, translate: true },
+    { name: 'ui_color',   display: __('UI color'), tag: 'select',   null: true, nulloption: true, translate: true, options: { 'low-priority': __('Low priority'), 'high-priority': __('High priority') }, note: __('Defines an optional highlight color of this priority in ticket tables. High priority will be rendered in an indian red, while low priority in a baby blue color.') },
+    { name: 'ui_icon',    display: __('UI icon'),  tag: 'select',   null: true, nulloption: true, translate: true, options: { 'low-priority': __('Low priority'), 'important': __('Important') }, note: __('Defines an optional icon of this priority in ticket tables. Important will be rendered with an exclamation point, while low priority with a downwards arrow.') },
+    { name: 'note',       display: __('Note'),     tag: 'textarea', limit: 250, null: true },
+    { name: 'active',     display: __('Active'),   tag: 'active',   default: true },
+    { name: 'updated_at', display: __('Updated'),  tag: 'datetime', readonly: 1 },
+    { name: 'created_at', display: __('Created'),  tag: 'datetime', readonly: 1 },
   ]
+  @configure_clone = true
   @configure_translate = true
   @configure_overview = [
     'name',
   ]
+
+  @description = __('''
+A ticket's priority is simply a ranking of how urgent or important it is. Different priorities allow you to see the importance of your tickets better.
+''')
+
+  @badges = [
+    {
+      display: __('Default for new tickets'),
+      active: (object) ->
+        object.default_create
+      attribute: 'name'
+      class: 'primary'
+    }
+  ]

+ 32 - 5
app/assets/javascripts/app/models/ticket_state.coffee

@@ -1,16 +1,43 @@
 class App.TicketState extends App.Model
-  @configure 'TicketState', 'name', 'note', 'active'
+  @configure 'TicketState', 'name', 'state_type_id', 'next_state_id', 'default_create', 'default_follow_up', 'ignore_escalation', 'note', 'active', 'updated_at'
   @extend Spine.Model.Ajax
   @url: @apiPath + '/ticket_states'
   @configure_attributes = [
-    { name: 'name',       display: __('Name'),    tag: 'input',     type: 'text', limit: 100, null: false, translate: true },
-    { name: 'active',     display: __('Active'),  tag: 'active',    default: true },
-    { name: 'updated_at', display: __('Updated'), tag: 'datetime',  readonly: 1 },
-    { name: 'created_at', display: __('Created'), tag: 'datetime',  readonly: 1 },
+    { name: 'name',                 display: __('Name'),                tag: 'input',     type: 'text', limit: 100, null: false, translate: true },
+    { name: 'state_type_id',        display: __('Type'),                tag: 'select',    null: false, relation: 'TicketStateType', nulloption: true, help: __('Zammad uses state types to know what it should do with your state. This allows you to have different types like pending actions, pending reminders or closed states.'), translate: true },
+    { name: 'next_state_id',        display: __('Next State'),          tag: 'select',    null: true, relation: 'TicketState', nulloption: true },
+    { name: 'ignore_escalation',    display: __('Ignore Escalation'),   tag: 'boolean',   null: false, default: false },
+    { name: 'note',                 display: __('Note'),                tag: 'textarea',  limit: 250, null: true },
+    { name: 'active',               display: __('Active'),              tag: 'active',    default: true },
+    { name: 'updated_at',           display: __('Updated'),             tag: 'datetime',  readonly: 1 },
+    { name: 'created_at',           display: __('Created'),             tag: 'datetime',  readonly: 1 },
   ]
+  @configure_clone = true
   @configure_translate = true
   @configure_overview = [
     'name',
+    'state_type_id',
+  ]
+
+  @description = __('''
+A ticket's state is used to categorize and manage the lifecycle of a ticket or customer inquiry.
+''')
+
+  @badges = [
+    {
+      display: __('Default for new tickets')
+      active: (object) ->
+        object.default_create
+      attribute: 'name'
+      class: 'primary'
+    },
+    {
+      display: __('Default for follow-ups')
+      active: (object) ->
+        object.default_follow_up
+      attribute: 'name'
+      class: 'primary'
+    }
   ]
 
   @byCategory: (category) ->

+ 2 - 0
app/assets/javascripts/app/models/ticket_state_type.coffee

@@ -2,3 +2,5 @@ class App.TicketStateType extends App.Model
   @configure 'TicketStateType', 'name', 'note', 'active', 'updated_at'
   @extend Spine.Model.Ajax
   @url: @apiPath + '/ticket_state_types'
+
+  @configure_translate = true

+ 1 - 1
app/assets/javascripts/app/views/generic/navbar_level2/navbar.jst.eco

@@ -5,7 +5,7 @@
       <ul class="nav nav-pills nav-stacked">
       <% if group.items: %>
         <% for item in group.items: %>
-          <li <% if item.active: %>class="active js-item"<% end %>><a href="<%= item.target %>"><%- @T(item.name) %></a></li>
+          <li class="<% if item.active: %>active js-item<% end %> <% if item.hidden: %>hidden<% end %>"><a href="<%= item.target %>"><%- @T(item.name) %></a></li>
         <% end %>
       <% end %>
       </ul>

+ 7 - 0
app/assets/javascripts/app/views/generic/table_row.jst.eco

@@ -78,6 +78,13 @@
             <span class="badge badge--primary"><%= @T('Default') %></span>
           <% end %>
          <% end %>
+        <% if @object.constructor.badges && @object.constructor.badges.length > 0: %>
+          <% for badge in @object.constructor.badges: %>
+            <% if badge.attribute == header.name && badge.active(@object): %>
+              <span class="badge badge--<%= badge.class %>"><%= @T(badge.display) %></span>
+            <% end %>
+          <% end %>
+        <% end %>
         <% if header.class || header.data: %></span><% end %>
       <% end %>
       <% if header.link: %></a><% end %>

+ 4 - 0
app/assets/javascripts/app/views/object_manager/index.jst.eco

@@ -80,6 +80,10 @@
               <span class="is-disabled" title="<%= item.not_deletable_reason %>"><%- @Icon('trash') %></span>
             <% end %>
           <% end %>
+          <% manageRoute = item.manageRoute() %>
+          <% if manageRoute: %>
+            <a href="<%= manageRoute %>" title="<%- @Ti('Manage') %>"><%- @Icon('cog', 'u-highlight') %></a>
+          <% end %>
         </td>
       </tr>
       <% end %>

+ 1 - 1
app/assets/stylesheets/zammad.scss

@@ -1627,7 +1627,7 @@ td.align-right {
 .table tr.is-grayed-out {
   color: var(--text-muted);
 
-  .icon,
+  .icon:not(.u-highlight),
   .btn span {
     opacity: 0.33;
   }

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