Merge branch 'develop' of github.com:martini/zammad into develop

This commit is contained in:
Felix Niklas 2015-10-15 15:28:43 +02:00
commit 9c27582068
22 changed files with 438 additions and 15 deletions

View file

@ -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

View file

@ -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 = $("<select class=\"form-control\" name=\"#{name}\"></select>")
for operatorKey in operators[groupAndAttribute]
operatorKeyName = App.i18n.translateInline(operatorKey)
selected = ''
if operatorKey is operator
selected = 'selected'
selection.append("<option value=\"#{operatorKey}\" #{selected}>#{operatorKeyName}</option>")
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

View file

@ -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' )

View file

@ -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']

View file

@ -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')

View file

@ -50,14 +50,14 @@ class App.TicketZoomSidebar extends App.Controller
if !@isRole('Customer')
el.append('<div class="tags"></div>')
new App.WidgetTag(
@tagWidget = new App.WidgetTag(
el: el.find('.tags')
object_type: 'Ticket'
object: ticket
tags: @tags
)
el.append('<div class="links"></div>')
new App.WidgetLink(
@linkWidget = new App.WidgetLink(
el: el.find('.links')
object_type: 'Ticket'
object: ticket

View file

@ -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()

View file

@ -26,6 +26,9 @@ class App.PrettyDate
diff = diff.toString().replace('-', '')
diff = parseFloat(diff)
if diff < 60
return App.i18n.translateInline('just now')
if direction is 'past' && !escalation && diff > ( 60 * 60 * 24 * 14 )
return App.i18n.translateDate(time)

View file

@ -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....
'''

View file

@ -6,6 +6,7 @@
<%- @Icon('arrow-down', 'dropdown-arrow') %>
</div>
</div>
<div class="controls js-operator"></div>
<div class="controls js-value"></div>
</div>
<div class="filter-controls">

View file

@ -634,7 +634,7 @@
</div>
</div>
<div class="attributeBar">
<div class="btn js-reset hide">Discard your unsaved changes.</div>
<div class="btn js-reset">Discard your unsaved changes.</div>
<div class="buttonDropdown dropdown dropup">
<div class="btn btn--primary btn--split--first js-submit">Update</div>
<div class="btn btn--primary btn--slim btn--split--last js-openDropdown"><%- @Icon('arrow-up') %></div>

View file

@ -25,8 +25,5 @@
<div class="tabsSidebar tabsSidebar--attributeBarSpacer vertical"></div>
<div class="attributeBar">
<div class="btn js-reset hide"><%- @T('Discard your unsaved changes.') %></div>
<div class="btn btn--primary js-submit"><%- @T('Update') %></div>
</div>
<div class="attributeBar js-attributeBar"></div>
</div>

View file

@ -0,0 +1,14 @@
<div class="btn js-reset hide"><%- @T('Discard your unsaved changes.') %></div>
<% if @macroDisabled: %>
<div class="btn btn--primary js-submit"><%- @T('Update') %></div>
<% else: %>
<div class="buttonDropdown dropdown dropup">
<div class="btn btn--primary btn--split--first js-submit"><%- @T('Update') %></div>
<div class="btn btn--primary btn--slim btn--split--last js-openDropdownMacro"><%- @Icon('arrow-up') %></div>
<ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="userAction">
<% for macro in @macros: %>
<li class="js-dropdownActionMacro" role="menuitem" data-id="<%= macro.id %>"><%- @T(macro.displayName()) %>
<% end %>
</ul>
</div>
<% end %>

View file

@ -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

View file

@ -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)

7
app/models/macro.rb Normal file
View file

@ -0,0 +1,7 @@
# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/
class Macro < ApplicationModel
store :perform
validates :name, presence: true
end

11
config/routes/macro.rb Normal file
View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -0,0 +1,4 @@
class Sessions::Backend::Collections::Macors < Sessions::Backend::Collections::Base
model_set 'Macro'
add_if_not_role 'Customer'
end

View file

@ -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' );

View file

@ -5,7 +5,10 @@ test( "check pretty date", function() {
// past
var result = App.PrettyDate.humanTime( current );
equal( result, '0 minutes ago', 'right now')
equal( result, 'just now', 'just now')
result = App.PrettyDate.humanTime( current - 15000 );
equal( result, 'just now', 'just now')
result = App.PrettyDate.humanTime( current - 60000 );
equal( result, '1 minute ago', '1 min ago')
@ -60,7 +63,10 @@ test( "check pretty date", function() {
// future
current = new Date()
result = App.PrettyDate.humanTime( current );
equal( result, '0 minutes ago', 'right now')
equal( result, 'just now', 'just now')
result = App.PrettyDate.humanTime( current.getTime() + 55000 );
equal( result, 'just now', 'just now')
result = App.PrettyDate.humanTime( current.getTime() + 65000 );
equal( result, 'in 1 minute', 'in 1 min')