diff --git a/app/assets/javascripts/app/controllers/_application_controller.coffee b/app/assets/javascripts/app/controllers/_application_controller.coffee index 522748b13..5de30d05b 100644 --- a/app/assets/javascripts/app/controllers/_application_controller.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller.coffee @@ -528,6 +528,9 @@ class App.Controller extends Spine.Controller # replace new option list form.find('[name="' + fieldNameToChange + '"]').closest('.form-group').replaceWith( newElement ) + stopPropagation: (e) -> + e.stopPropagation() + class App.ControllerPermanent extends App.Controller constructor: -> super diff --git a/app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee b/app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee index a59572b8f..ea6562a43 100644 --- a/app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee +++ b/app/assets/javascripts/app/controllers/_ui_element/ticket_perform_action.coffee @@ -8,6 +8,9 @@ class App.UiElement.ticket_perform_action name: 'Ticket' model: 'Ticket' + operators = + 'ticket.tags': ['add', 'remove'] + # megre config elements = {} for groupKey, groupMeta of groups @@ -21,11 +24,11 @@ class App.UiElement.ticket_perform_action config = _.clone(row) elements["#{groupKey}.#{config.name}"] = config - [defaults, groups, elements] + [defaults, groups, operators, elements] @render: (attribute, params = {}) -> - [defaults, groups, elements] = @defaults() + [defaults, groups, operators, elements] = @defaults() selector = @buildAttributeSelector(groups, elements) @@ -53,6 +56,7 @@ class App.UiElement.ticket_perform_action elementRow = $(e.target).closest('.js-filterElement') @rebuildAttributeSelectors(item, elementRow, groupAndAttribute) + @buildOperator(item, elementRow, groupAndAttribute, elements, undefined, attribute, operators) @buildValue(item, elementRow, groupAndAttribute, elements, undefined, attribute) ) @@ -63,6 +67,7 @@ class App.UiElement.ticket_perform_action for groupAndAttribute, meta of params[attribute.name] selectorExists = true value = meta.value + operator = meta.operator # get selector rows elementFirst = item.find('.js-filterElement').first() @@ -71,6 +76,7 @@ class App.UiElement.ticket_perform_action # clone, rebuild and append elementClone = elementFirst.clone(true) @rebuildAttributeSelectors(item, elementClone, groupAndAttribute) + @buildOperator(item, elementClone, groupAndAttribute, elements, value, attribute, operators, operator) @buildValue(item, elementClone, groupAndAttribute, elements, value, attribute) elementLast.after(elementClone) @@ -93,6 +99,26 @@ class App.UiElement.ticket_perform_action item + @buildOperator: (elementFull, elementRow, groupAndAttribute, elements, value, attribute, operators, operator) -> + name = "#{attribute.name}::#{groupAndAttribute}::operator" + if !operators[groupAndAttribute] + elementRow.find('.js-operator').html('') + return + + # get current operator + if !operator + operator = elementRow.find('.js-operator select').val() + + # build new operator + selection = $("") + for operatorKey in operators[groupAndAttribute] + operatorKeyName = App.i18n.translateInline(operatorKey) + selected = '' + if operatorKey is operator + selected = 'selected' + selection.append("") + elementRow.find('.js-operator').html(selection) + @buildValue: (elementFull, elementRow, groupAndAttribute, elements, value, attribute) -> # do nothing if item already exists @@ -163,7 +189,7 @@ class App.UiElement.ticket_perform_action @humanText: (condition) -> none = App.i18n.translateContent('No filter.') return [none] if _.isEmpty(condition) - [defaults, groups, elements] = @defaults() + [defaults, groups, operators, elements] = @defaults() rules = [] for attribute, value of condition diff --git a/app/assets/javascripts/app/controllers/macro.coffee b/app/assets/javascripts/app/controllers/macro.coffee new file mode 100644 index 000000000..d6d2c0e18 --- /dev/null +++ b/app/assets/javascripts/app/controllers/macro.coffee @@ -0,0 +1,27 @@ +class Index extends App.ControllerContent + constructor: -> + super + + # check authentication + return if !@authenticate() + + new App.ControllerGenericIndex( + el: @el + id: @id + genericObject: 'Macro' + pageData: + title: 'Macros' + home: 'macros' + object: 'Macro' + objects: 'Macros' + navupdate: '#macros' + notes: [ + 'TextModules are ...' + ] + buttons: [ + { name: 'New Macro', 'data-type': 'new', class: 'btn--success' } + ] + container: @el.closest('.content') + ) + +App.Config.set( 'Macros', { prio: 2310, name: 'Macros', parent: '#manage', target: '#manage/macros', controller: Index, role: ['Admin'] }, 'NavBarAdmin' ) \ No newline at end of file diff --git a/app/assets/javascripts/app/controllers/ticket_zoom.coffee b/app/assets/javascripts/app/controllers/ticket_zoom.coffee index f8fa10cc6..b0d846ec1 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom.coffee @@ -308,6 +308,12 @@ class App.TicketZoom extends App.Controller el: @el.find('.ticket-meta') ) + new App.TicketZoomAttributeBar( + ticket: @ticket + el: @el.find('.js-attributeBar') + callback: @submit + ) + @form_id = App.ControllerForm.formId() new App.TicketZoomArticleNew( @@ -338,7 +344,7 @@ class App.TicketZoom extends App.Controller user_id: @ticket.customer_id size: 50 ) - new App.TicketZoomSidebar( + @sidebar = new App.TicketZoomSidebar( el: @el.find('.tabsSidebar') sidebarState: @sidebarState ticket: @ticket @@ -462,7 +468,7 @@ class App.TicketZoom extends App.Controller resetButton.removeClass('hide') - submit: (e) => + submit: (e, macro = {}) => e.stopPropagation() e.preventDefault() ticketParams = @formParam( @$('.edit') ) @@ -477,6 +483,25 @@ class App.TicketZoom extends App.Controller for key, value of ticketParams ticket[key] = value + # apply macro + for key, content of macro + attributes = key.split('.') + if attributes[0] is 'ticket' + + # apply tag changes + if attributes[1] is 'tags' + if @sidebar && @sidebar.tagWidget + tags = content.value.split(',') + for tag in tags + if content.operator is 'remove' + @sidebar.tagWidget.remove(tag) + else + @sidebar.tagWidget.add(tag) + + # apply direct value changes + else + ticket[attributes[1]] = content.value + # set defaults if !@isRole('Customer') if !ticket['owner_id'] diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/attribute_bar.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/attribute_bar.coffee new file mode 100644 index 000000000..4fc23b084 --- /dev/null +++ b/app/assets/javascripts/app/controllers/ticket_zoom/attribute_bar.coffee @@ -0,0 +1,53 @@ +class App.TicketZoomAttributeBar extends App.Controller + elements: + '.buttonDropdown': 'buttonDropdown' + + events: + 'mousedown .js-openDropdownMacro': 'toggleDropdownMacro' + 'click .js-openDropdownMacro': 'stopPropagation' + 'mouseup .js-dropdownActionMacro': 'performTicketMacro' + 'mouseenter .js-dropdownActionMacro': 'onActionMacroMouseEnter' + 'mouseleave .js-dropdownActionMacro': 'onActionMacroMouseLeave' + + constructor: -> + super + + @subscribeId = App.Macro.subscribe(@render) + @render() + + release: => + App.Macro.unsubscribe(@subscribeId) + + render: => + macros = App.Macro.all() + if _.isEmpty(macros) || !@isRole('Agent') + macroDisabled = true + @html App.view('ticket_zoom/attribute_bar')( + macros: macros + macroDisabled: macroDisabled + ) + + toggleDropdownMacro: => + if @buttonDropdown.hasClass 'is-open' + @closeMacroDropdown() + else + @buttonDropdown.addClass 'is-open' + $(document).bind 'click.buttonDropdown', @closeMacroDropdown + + closeMacroDropdown: => + @buttonDropdown.removeClass 'is-open' + $(document).unbind 'click.buttonDropdown' + + performTicketMacro: (e) => + macroId = $(e.target).data('id') + console.log "perform action", @$(e.currentTarget).text(), macroId + macro = App.Macro.find(macroId) + + @callback(e, macro.perform) + @closeMacroDropdown() + + onActionMacroMouseEnter: (e) => + @$(e.currentTarget).addClass('is-active') + + onActionMacroMouseLeave: (e) => + @$(e.currentTarget).removeClass('is-active') \ No newline at end of file diff --git a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar.coffee b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar.coffee index 4e9c5c93c..9395e5d75 100644 --- a/app/assets/javascripts/app/controllers/ticket_zoom/sidebar.coffee +++ b/app/assets/javascripts/app/controllers/ticket_zoom/sidebar.coffee @@ -50,14 +50,14 @@ class App.TicketZoomSidebar extends App.Controller if !@isRole('Customer') el.append('
') - new App.WidgetTag( + @tagWidget = new App.WidgetTag( el: el.find('.tags') object_type: 'Ticket' object: ticket tags: @tags ) el.append('') - new App.WidgetLink( + @linkWidget = new App.WidgetLink( el: el.find('.links') object_type: 'Ticket' object: ticket diff --git a/app/assets/javascripts/app/controllers/widget/tag.coffee b/app/assets/javascripts/app/controllers/widget/tag.coffee index 3ffa46f5d..a5d18fb15 100644 --- a/app/assets/javascripts/app/controllers/widget/tag.coffee +++ b/app/assets/javascripts/app/controllers/widget/tag.coffee @@ -65,8 +65,10 @@ class App.WidgetTag extends App.Controller e.preventDefault() item = @$('[name="new_tag"]').val() return if !item + @add(item) - if _.contains(@tagList, item) + add: (item) => + if _.contains(@tags, item) @render() return @@ -90,6 +92,10 @@ class App.WidgetTag extends App.Controller item = $(e.target).parents('li').find('.js-tag').text() return if !item + @remove(item) + + remove: (item) => + @tags = _.filter(@tags, (tagItem) -> return tagItem if tagItem isnt item ) @render() diff --git a/app/assets/javascripts/app/models/macro.coffee b/app/assets/javascripts/app/models/macro.coffee new file mode 100644 index 000000000..99a60c96f --- /dev/null +++ b/app/assets/javascripts/app/models/macro.coffee @@ -0,0 +1,20 @@ +class App.Macro extends App.Model + @configure 'Macro', 'name', 'perform', 'active' + @extend Spine.Model.Ajax + @url: @apiPath + '/macros' + @configure_attributes = [ + { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, null: false }, + { name: 'perform', display: 'Execute changes on objects.', tag: 'ticket_perform_action', null: true }, + { name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 }, + { name: 'note', display: 'Note', tag: 'textarea', limit: 250, null: true }, + { name: 'active', display: 'Active', tag: 'active', default: true }, + ] + @configure_delete = true + @configure_overview = [ + 'name', + ] + + @description = ''' +Macros are.... + +''' \ No newline at end of file diff --git a/app/assets/javascripts/app/views/generic/ticket_perform_action.jst.eco b/app/assets/javascripts/app/views/generic/ticket_perform_action.jst.eco index 17e5c88e0..4f9566af6 100644 --- a/app/assets/javascripts/app/views/generic/ticket_perform_action.jst.eco +++ b/app/assets/javascripts/app/views/generic/ticket_perform_action.jst.eco @@ -6,6 +6,7 @@ <%- @Icon('arrow-down', 'dropdown-arrow') %> +
diff --git a/app/assets/javascripts/app/views/layout_ref/ticket_zoom.jst.eco b/app/assets/javascripts/app/views/layout_ref/ticket_zoom.jst.eco index d84787352..786d6a9b7 100644 --- a/app/assets/javascripts/app/views/layout_ref/ticket_zoom.jst.eco +++ b/app/assets/javascripts/app/views/layout_ref/ticket_zoom.jst.eco @@ -634,7 +634,7 @@
-
Discard your unsaved changes.
+
Discard your unsaved changes.
\ No newline at end of file diff --git a/app/assets/javascripts/app/views/ticket_zoom/attribute_bar.jst.eco b/app/assets/javascripts/app/views/ticket_zoom/attribute_bar.jst.eco new file mode 100644 index 000000000..213d6daf4 --- /dev/null +++ b/app/assets/javascripts/app/views/ticket_zoom/attribute_bar.jst.eco @@ -0,0 +1,14 @@ +
<%- @T('Discard your unsaved changes.') %>
+<% if @macroDisabled: %> +
<%- @T('Update') %>
+<% else: %> + +<% end %> \ No newline at end of file diff --git a/app/controllers/macros_controller.rb b/app/controllers/macros_controller.rb new file mode 100644 index 000000000..db0856694 --- /dev/null +++ b/app/controllers/macros_controller.rb @@ -0,0 +1,155 @@ +# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/ + +class MacrosController < ApplicationController + before_action :authentication_check + +=begin + +Format: +JSON + +Example: +{ + "id":1, + "name":"some text_module", + "perform":{ + "ticket.priority_id": 5, + "ticket.state_id": 2, + }, + "active":true, + "updated_at":"2012-09-14T17:51:53Z", + "created_at":"2012-09-14T17:51:53Z", + "updated_by_id":2, + "created_by_id":2, +} + +=end + +=begin + +Resource: +GET /api/v1/macros.json + +Response: +[ + { + "id": 1, + "name": "some_name1", + ... + }, + { + "id": 2, + "name": "some_name2", + ... + } +] + +Test: +curl http://localhost/api/v1/macros.json -v -u #{login}:#{password} + +=end + + def index + model_index_render(Macro, params) + end + +=begin + +Resource: +GET /api/v1/macros/#{id}.json + +Response: +{ + "id": 1, + "name": "name_1", + ... +} + +Test: +curl http://localhost/api/v1/macros/#{id}.json -v -u #{login}:#{password} + +=end + + def show + model_show_render(Macro, params) + end + +=begin + +Resource: +POST /api/v1/macros.json + +Payload: +{ + "name": "some name", + "perform":{ + "ticket.priority_id": 5, + "ticket.state_id": 2, + }, + "active":true, +} + +Response: +{ + "id": 1, + "name": "some_name", + ... +} + +Test: +curl http://localhost/api/v1/macros.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X POST -d '{"name": "some_name","active": true, "note": "some note"}' + +=end + + def create + model_create_render(Macro, params) + end + +=begin + +Resource: +PUT /api/v1/macros/{id}.json + +Payload: +{ + "name": "some name", + "perform":{ + "ticket.priority_id": 5, + "ticket.state_id": 2, + }, + "active":true, +} + +Response: +{ + "id": 1, + "name": "some_name", + ... +} + +Test: +curl http://localhost/api/v1/macros.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X PUT -d '{"name": "some_name","active": true, "note": "some note"}' + +=end + + def update + model_update_render(Macro, params) + end + +=begin + +Resource: +DELETE /api/v1/macros/{id}.json + +Response: +{} + +Test: +curl http://localhost/api/v1/macros.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X DELETE + +=end + + def destroy + model_destory_render(Macro, params) + end +end diff --git a/app/controllers/sessions/collection_ticket.rb b/app/controllers/sessions/collection_ticket.rb index 63a5ea455..201ef3b1a 100644 --- a/app/controllers/sessions/collection_ticket.rb +++ b/app/controllers/sessions/collection_ticket.rb @@ -4,6 +4,10 @@ module ExtraCollection def session( collections, assets, user ) # all ticket stuff + collections[ Macro.to_app_model ] = [] + Macro.all.each {|item| + assets = item.assets(assets) + } collections[ Ticket::StateType.to_app_model ] = [] Ticket::StateType.all.each {|item| assets = item.assets(assets) diff --git a/app/models/macro.rb b/app/models/macro.rb new file mode 100644 index 000000000..a308cf61c --- /dev/null +++ b/app/models/macro.rb @@ -0,0 +1,7 @@ +# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/ + +class Macro < ApplicationModel + store :perform + validates :name, presence: true + +end diff --git a/config/routes/macro.rb b/config/routes/macro.rb new file mode 100644 index 000000000..93f04c418 --- /dev/null +++ b/config/routes/macro.rb @@ -0,0 +1,11 @@ +Zammad::Application.routes.draw do + api_path = Rails.configuration.api_path + + # macros + match api_path + '/macros', to: 'macros#index', via: :get + match api_path + '/macros/:id', to: 'macros#show', via: :get + match api_path + '/macros', to: 'macros#create', via: :post + match api_path + '/macros/:id', to: 'macros#update', via: :put + match api_path + '/macros/:id', to: 'macros#destroy', via: :delete + +end diff --git a/db/migrate/20151015000001_create_macro.rb b/db/migrate/20151015000001_create_macro.rb new file mode 100644 index 000000000..21214b7db --- /dev/null +++ b/db/migrate/20151015000001_create_macro.rb @@ -0,0 +1,34 @@ +class CreateMacro < ActiveRecord::Migration + def up + create_table :macros do |t| + t.string :name, limit: 250, null: true + t.string :perform, limit: 5000, null: false + t.boolean :active, null: false, default: true + t.string :note, limit: 250, null: true + t.integer :updated_by_id, null: false + t.integer :created_by_id, null: false + t.timestamps null: false + end + add_index :macros, [:name], unique: true + + UserInfo.current_user_id = 1 + Macro.create_or_update( + name: 'Close & Tag as Spam', + perform: { + 'ticket.state_id': { + value: Ticket::State.find_by(name: 'closed').id, + }, + 'ticket.tags': { + operator: 'add', + value: 'spam', + }, + }, + note: 'example macro', + active: true, + ) + end + + def down + drop_table :macros + end +end diff --git a/db/seeds.rb b/db/seeds.rb index 920750e30..6bf34ec19 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1572,6 +1572,21 @@ Ticket::Article::Sender.create_if_not_exists( id: 1, name: 'Agent' ) Ticket::Article::Sender.create_if_not_exists( id: 2, name: 'Customer' ) Ticket::Article::Sender.create_if_not_exists( id: 3, name: 'System' ) +Macro.create_if_not_exists( + name: 'Close & Tag as Spam', + perform: { + 'ticket.state_id': { + value: Ticket::State.find_by(name: 'closed').id, + }, + 'ticket.tags': { + operator: 'add', + value: 'spam', + }, + }, + note: 'example macro', + active: true, +) + UserInfo.current_user_id = user_community.id ticket = Ticket.create( group_id: Group.where( name: 'Users' ).first.id, diff --git a/lib/sessions/backend/collections/macro.rb b/lib/sessions/backend/collections/macro.rb new file mode 100644 index 000000000..c62bb1122 --- /dev/null +++ b/lib/sessions/backend/collections/macro.rb @@ -0,0 +1,4 @@ +class Sessions::Backend::Collections::Macors < Sessions::Backend::Collections::Base + model_set 'Macro' + add_if_not_role 'Customer' +end diff --git a/public/assets/tests/form-extended.js b/public/assets/tests/form-extended.js index c68906182..1e688067e 100644 --- a/public/assets/tests/form-extended.js +++ b/public/assets/tests/form-extended.js @@ -137,6 +137,10 @@ test( 'form checks', function() { 'ticket.priority_id': { value: 3, }, + 'ticket.tags': { + operator: 'remove', + value: 'tag1, tag2', + }, }, } new App.ControllerForm({ @@ -193,6 +197,10 @@ test( 'form checks', function() { 'ticket.priority_id': { value: '3', }, + 'ticket.tags': { + operator: 'remove', + value: 'tag1, tag2', + }, }, working_hours: { mon: { @@ -328,6 +336,10 @@ test( 'form checks', function() { 'ticket.priority_id': { value: '3', }, + 'ticket.tags': { + operator: 'remove', + value: 'tag1, tag2', + }, }, } deepEqual( params, test_params, 'form param check' );