Improved rendering of profile pages. Added touch to customer and organization if ticket has changed.

This commit is contained in:
Martin Edenhofer 2015-02-06 09:59:08 +01:00
parent d46196b69e
commit 412f868d42
13 changed files with 655 additions and 122 deletions

View file

@ -1,7 +1,4 @@
class App.OrganizationProfile extends App.Controller
events:
'focusout [contenteditable]': 'update'
constructor: (params) ->
super
@ -12,11 +9,8 @@ class App.OrganizationProfile extends App.Controller
@navupdate '#'
# subscribe and reload data / fetch new data if triggered
@subscribeId = App.Organization.full( @organization_id, @render, false, true )
release: =>
App.Organization.unsubscribe(@subscribeId)
# fetch new data if needed
App.Organization.full( @organization_id, @render )
meta: =>
meta =
@ -48,6 +42,39 @@ class App.OrganizationProfile extends App.Controller
@doNotLog = 1
@recentView( 'Organization', @organization_id )
@html App.view('organization_profile/index')(
organization: organization
)
new Object(
el: @$('.js-object-container')
organization: organization
)
new App.TicketStats(
el: @$('.js-ticket-stats')
organization: organization
)
new App.UpdateTastbar(
genericObject: organization
)
class Object extends App.Controller
events:
'focusout [contenteditable]': 'update'
constructor: (params) ->
super
# subscribe and reload data / fetch new data if triggered
@subscribeId = App.Organization.full( @organization.id, @render, false, true )
release: =>
App.Organization.unsubscribe(@subscribeId)
render: (organization) =>
# get display data
organizationData = []
for attributeName, attributeConfig of App.Organization.attributesGet('view')
@ -65,7 +92,7 @@ class App.OrganizationProfile extends App.Controller
if name isnt 'name'
organizationData.push attributeConfig
@html App.view('organization_profile')(
@html App.view('organization_profile/object')(
organization: organization
organizationData: organizationData
)
@ -76,15 +103,6 @@ class App.OrganizationProfile extends App.Controller
maxlength: 250
})
new App.TicketStats(
el: @$('.js-ticket-stats')
organization: organization
)
new App.UpdateTastbar(
genericObject: organization
)
# start action controller
showHistory = =>
new App.OrganizationHistory( organization_id: organization.id )
@ -97,6 +115,7 @@ class App.OrganizationProfile extends App.Controller
title: 'Organizations'
object: 'Organization'
objects: 'Organizations'
container: @el.closest('.content')
)
actions = [
@ -120,13 +139,14 @@ class App.OrganizationProfile extends App.Controller
update: (e) =>
name = $(e.target).attr('data-name')
value = $(e.target).html()
org = App.Organization.find( @organization_id )
org = App.Organization.find( @organization.id )
if org[name] isnt value
data = {}
data[name] = value
org.updateAttributes( data )
@log 'notice', 'update', name, value, org
class Router extends App.ControllerPermanent
constructor: (params) ->
super

View file

@ -1,7 +1,4 @@
class App.UserProfile extends App.Controller
events:
'focusout [contenteditable]': 'update'
constructor: (params) ->
super
@ -12,8 +9,8 @@ class App.UserProfile extends App.Controller
@navupdate '#'
# subscribe and reload data / fetch new data if triggered
@subscribeId = App.User.full( @user_id, @render, false, true )
# fetch new data if needed
@subscribeId = App.User.full( @user_id, @render )
release: =>
App.User.unsubscribe(@subscribeId)
@ -47,6 +44,40 @@ class App.UserProfile extends App.Controller
@doNotLog = 1
@recentView( 'User', @user_id )
@html App.view('user_profile/index')(
user: user
)
new Object(
el: @$('.js-object-container')
user: user
)
new App.TicketStats(
el: @$('.js-ticket-stats')
user: user
)
new App.UpdateTastbar(
genericObject: user
)
class Object extends App.Controller
events:
'focusout [contenteditable]': 'update'
constructor: (params) ->
super
# subscribe and reload data / fetch new data if triggered
@subscribeId = App.User.full( @user.id, @render, false, true )
release: =>
App.User.unsubscribe(@subscribeId)
render: (user) =>
# get display data
userData = []
for attributeName, attributeConfig of App.User.attributesGet('view')
@ -64,7 +95,7 @@ class App.UserProfile extends App.Controller
if name isnt 'firstname' && name isnt 'lastname' && name isnt 'organization'
userData.push attributeConfig
@html App.view('user_profile')(
@html App.view('user_profile/object')(
user: user
userData: userData
)
@ -75,15 +106,6 @@ class App.UserProfile extends App.Controller
maxlength: 250
})
new App.TicketStats(
el: @$('.js-ticket-stats')
user: user
)
new App.UpdateTastbar(
genericObject: user
)
# start action controller
showHistory = =>
new App.UserHistory( user_id: user.id )
@ -97,6 +119,7 @@ class App.UserProfile extends App.Controller
title: 'Users'
object: 'User'
objects: 'Users'
container: @el.closest('.content')
)
actions = [
@ -120,7 +143,7 @@ class App.UserProfile extends App.Controller
update: (e) =>
name = $(e.target).attr('data-name')
value = $(e.target).html()
user = App.User.find( @user_id )
user = App.User.find( @user.id )
if user[name] isnt value
data = {}
data[name] = value

View file

@ -1,50 +0,0 @@
<div class="flex profile">
<div class="profile-window">
<div class="profile-section vertical centered">
<div class="align-right profile-action js-action"></div>
<div class="profile-organizationIcon">
<div class="organization icon"></div>
</div>
<h1><%= @organization.displayName() %></h1>
</div>
<div class="profile-section">
<div class="profile-details horizontal wrap">
<% for row in @organizationData: %>
<% if @organization[row.name]: %>
<% if row.tag is 'richtext': %>
<div class="profile-detailsEntry" style="width: 100%;">
<label><%- @Ti( row.display ) %></label>
<div contenteditable="true" data-name="<%= row.name %>" data-type="update" data-placeholder="<%- @T('Add a Note') %>"><%- @organization[row.name] %></div>
</div>
<% else: %>
<div class="profile-detailsEntry">
<label><%- @Ti( row.display ) %></label>
<%- @L( @P( @organization[row.name] ) ) %>
</div>
<% end %>
<% end %>
<% end %>
</div>
</div>
<% if @organization.members: %>
<div class="profile-section profile-memberSection">
<label><%- @T('Members') %></label>
<div class="profile-details horizontal wrap">
<% for user in @organization.members: %>
<div class="profile-organizationMember">
<%- user.avatar("40") %>
<a href="<%- user.uiUrl() %>" class="user-popover" data-id="<%- user.id %>"><%= user.displayName() %></a>
</div>
<% end %>
</div>
</div>
<% end %>
<div class="profile-section js-ticket-stats"></div>
</div>
</div>

View file

@ -0,0 +1,6 @@
<div class="flex profile">
<div class="profile-window">
<div class="js-object-container"></div>
<div class="profile-section js-ticket-stats"></div>
</div>
</div>

View file

@ -0,0 +1,39 @@
<div class="profile-section vertical centered">
<div class="align-right profile-action js-action"></div>
<div class="profile-organizationIcon">
<div class="organization icon"></div>
</div>
<h1><%= @organization.displayName() %></h1>
</div>
<div class="profile-section">
<div class="profile-details horizontal wrap">
<% for row in @organizationData: %>
<% if row.tag is 'richtext': %>
<div class="profile-detailsEntry" style="width: 100%;">
<label><%- @Ti( row.display ) %></label>
<div contenteditable="true" data-name="<%= row.name %>" data-type="update" data-placeholder="<%- @T('Add a Note') %>"><%- @organization[row.name] %></div>
</div>
<% else: %>
<% if @organization[row.name]: %>
<div class="profile-detailsEntry">
<label><%- @Ti( row.display ) %></label>
<%- @L( @P( @organization[row.name] ) ) %>
</div>
<% end %>
<% end %>
<% end %>
</div>
</div>
<% if @organization.members: %>
<div class="profile-section profile-memberSection">
<label><%- @T('Members') %></label>
<div class="profile-details horizontal wrap">
<% for user in @organization.members: %>
<div class="profile-organizationMember">
<%- user.avatar("40") %>
<a href="<%- user.uiUrl() %>" class="user-popover" data-id="<%- user.id %>"><%= user.displayName() %></a>
</div>
<% end %>
</div>
</div>
<% end %>

View file

@ -1,37 +0,0 @@
<div class="flex profile">
<div class="profile-window">
<div class="profile-section vertical centered">
<div class="align-right profile-action js-action"></div>
<%- @user.avatar("80") %>
<h1><%= @user.displayName() %></h1>
<% if @user.organization: %>
<div class="profile-organization"><a href="<%- @user.organization.uiUrl() %>"><%= @user.organization.displayName() %></a></div>
<% end %>
</div>
<div class="profile-section">
<div class="profile-details horizontal wrap">
<% for row in @userData: %>
<% if @user[row.name]: %>
<% if row.tag is 'richtext': %>
<div class="profile-detailsEntry" style="width: 100%;">
<label><%- @Ti( row.display ) %></label>
<div contenteditable="true" data-name="<%= row.name %>" data-type="update" data-placeholder="<%- @T('Add a Note') %>"><%- @user[row.name] %></div>
</div>
<% else: %>
<div class="profile-detailsEntry">
<label><%- @Ti( row.display ) %></label>
<%- @L( @P( @user[row.name] ) ) %>
</div>
<% end %>
<% end %>
<% end %>
</div>
</div>
<div class="profile-section js-ticket-stats"></div>
</div>
</div>

View file

@ -0,0 +1,6 @@
<div class="flex profile">
<div class="profile-window">
<div class="js-object-container"></div>
<div class="profile-section js-ticket-stats"></div>
</div>
</div>

View file

@ -0,0 +1,27 @@
<div class="profile-section vertical centered">
<div class="align-right profile-action js-action"></div>
<%- @user.avatar("80") %>
<h1><%= @user.displayName() %></h1>
<% if @user.organization: %>
<div class="profile-organization"><a href="<%- @user.organization.uiUrl() %>"><%= @user.organization.displayName() %></a></div>
<% end %>
</div>
<div class="profile-section">
<div class="profile-details horizontal wrap">
<% for row in @userData: %>
<% if @user[row.name]: %>
<% if row.tag is 'richtext': %>
<div class="profile-detailsEntry" style="width: 100%;">
<label><%- @Ti( row.display ) %></label>
<div contenteditable="true" data-name="<%= row.name %>" data-type="update" data-placeholder="<%- @T('Add a Note') %>"><%- @user[row.name] %></div>
</div>
<% else: %>
<div class="profile-detailsEntry">
<label><%- @Ti( row.display ) %></label>
<%- @L( @P( @user[row.name] ) ) %>
</div>
<% end %>
<% end %>
<% end %>
</div>
</div>

View file

@ -0,0 +1,30 @@
# Copyright (C) 2012-2014 Zammad Foundation, http://zammad-foundation.org/
class Observer::Ticket::RefObjectTouch < ActiveRecord::Observer
observe 'ticket'
def after_create(record)
ref_object_touch(record)
end
def after_update(record)
ref_object_touch(record)
end
def after_touch(record)
ref_object_touch(record)
end
def after_destroy(record)
ref_object_touch(record)
end
def ref_object_touch(record)
# return if we run import mode
return if Setting.get('import_mode')
if record.customer
record.customer.touch
end
if record.organization
record.organization.touch
end
end
end

View file

@ -40,6 +40,7 @@ module Zammad
'observer::_ticket::_notification',
'observer::_ticket::_reset_new_state',
'observer::_ticket::_escalation_calculation',
'observer::_ticket::_ref_object_touch',
'observer::_tag::_ticket_history',
'observer::_user::_geo'

View file

@ -0,0 +1,188 @@
# encoding: utf-8
require 'browser_test_helper'
class AgentOrganizationProfileTest < TestCase
def test_search_and_edit_verify_in_second
message = 'comment 1 ' + rand(99999999999999999).to_s
tests = [
{
:name => 'start',
:instance1 => browser_instance,
:instance2 => browser_instance,
:instance1_username => 'master@example.com',
:instance1_password => 'test',
:instance2_username => 'agent1@example.com',
:instance2_password => 'test',
:url => browser_url,
:action => [
{
:where => :instance1,
:execute => 'close_all_tasks',
},
{
:where => :instance2,
:execute => 'close_all_tasks',
},
{
:where => :instance1,
:execute => 'search_organization',
:term => 'Zammad',
},
{
:where => :instance2,
:execute => 'search_organization',
:term => 'Zammad',
},
# update note
{
:where => :instance1,
:execute => 'set',
:css => '.active [data-name="note"]',
:value => message,
},
{
:where => :instance1,
:execute => 'click',
:css => '.active .profile',
},
{
:where => :instance1,
:execute => 'wait',
:value => 3,
},
# verify
{
:where => :instance2,
:execute => 'match',
:css => '.active .profile-window',
:value => message,
:match_result => true,
},
],
},
]
browser_double_test(tests)
end
def test_search_and_edit_in_one
message = '1 ' + rand(99999999).to_s
tests = [
{
:name => 'search and edit',
:action => [
{
:execute => 'close_all_tasks',
},
# search and open org
{
:execute => 'search_organization',
:term => 'Zammad',
},
{
:execute => 'match',
:css => '.active .profile-window',
:value => 'note',
:match_result => true,
},
{
:execute => 'match',
:css => '.active .profile-window',
:value => 'member',
:match_result => true,
},
# update note
{
:execute => 'set',
:css => '.active [data-name="note"]',
:value => 'some note 123'
},
{
:execute => 'click',
:css => '.active .profile',
},
{
:execute => 'wait',
:value => 1,
},
# check and change note again in edit screen
{
:execute => 'click',
:css => '.active .js-action .select-arrow',
},
{
:execute => 'click',
:css => '.active .js-action a[data-type="edit"]',
},
{
:execute => 'wait',
:value => 1,
},
{
:execute => 'match',
:css => '.active .modal',
:value => 'note',
:match_result => true,
},
{
:execute => 'match',
:css => '.active .modal',
:value => 'some note 123',
:match_result => true,
},
{
:execute => 'set',
:css => '.active .modal [data-name="note"]',
:value => 'some note abc'
},
{
:execute => 'click',
:css => '.active .modal button.js-submit',
},
{
:execute => 'wait',
:value => 4,
},
{
:execute => 'match',
:css => '.active .profile-window',
:value => 'some note abc',
:match_result => true,
},
# create new ticket
{
:execute => 'create_ticket',
:group => 'Users',
:subject => 'org profile check ' + message,
:body => 'org profile check ' + message,
},
{
:execute => 'wait',
:value => 4,
},
# switch to org tab, verify if ticket is shown
{
:execute => 'search_organization',
:term => 'Zammad',
},
{
:execute => 'match',
:css => '.active .profile-window',
:value => 'org profile check ' + message,
:match_result => true,
},
],
},
]
browser_signle_test_with_login(tests, { :username => 'master@example.com' })
end
end

View file

@ -0,0 +1,188 @@
# encoding: utf-8
require 'browser_test_helper'
class AgentUserProfileTest < TestCase
def test_search_and_edit_verify_in_second
message = 'comment 1 ' + rand(99999999999999999).to_s
tests = [
{
:name => 'start',
:instance1 => browser_instance,
:instance2 => browser_instance,
:instance1_username => 'master@example.com',
:instance1_password => 'test',
:instance2_username => 'agent1@example.com',
:instance2_password => 'test',
:url => browser_url,
:action => [
{
:where => :instance1,
:execute => 'close_all_tasks',
},
{
:where => :instance2,
:execute => 'close_all_tasks',
},
{
:where => :instance1,
:execute => 'search_user',
:term => 'Braun',
},
{
:where => :instance2,
:execute => 'search_user',
:term => 'Braun',
},
# update note
{
:where => :instance1,
:execute => 'set',
:css => '.active [data-name="note"]',
:value => message,
},
{
:where => :instance1,
:execute => 'click',
:css => '.active .profile',
},
{
:where => :instance1,
:execute => 'wait',
:value => 3,
},
# verify
{
:where => :instance2,
:execute => 'match',
:css => '.active .profile-window',
:value => message,
:match_result => true,
},
],
},
]
browser_double_test(tests)
end
def test_search_and_edit_in_one
message = '1 ' + rand(99999999).to_s
tests = [
{
:name => 'search and edit',
:action => [
{
:execute => 'close_all_tasks',
},
# search and open user
{
:execute => 'search_user',
:term => 'Braun',
},
{
:execute => 'match',
:css => '.active .profile-window',
:value => 'note',
:match_result => true,
},
{
:execute => 'match',
:css => '.active .profile-window',
:value => 'email',
:match_result => true,
},
# update note
{
:execute => 'set',
:css => '.active [data-name="note"]',
:value => 'some note 123'
},
{
:execute => 'click',
:css => '.active .profile',
},
{
:execute => 'wait',
:value => 1,
},
# check and change note again in edit screen
{
:execute => 'click',
:css => '.active .js-action .select-arrow',
},
{
:execute => 'click',
:css => '.active .js-action a[data-type="edit"]',
},
{
:execute => 'wait',
:value => 1,
},
{
:execute => 'match',
:css => '.active .modal',
:value => 'note',
:match_result => true,
},
{
:execute => 'match',
:css => '.active .modal',
:value => 'some note 123',
:match_result => true,
},
{
:execute => 'set',
:css => '.active .modal [data-name="note"]',
:value => 'some note abc'
},
{
:execute => 'click',
:css => '.active .modal button.js-submit',
},
{
:execute => 'wait',
:value => 4,
},
{
:execute => 'match',
:css => '.active .profile-window',
:value => 'some note abc',
:match_result => true,
},
# create new ticket
{
:execute => 'create_ticket',
:group => 'Users',
:subject => 'user profile check ' + message,
:body => 'user profile check ' + message,
},
{
:execute => 'wait',
:value => 4,
},
# switch to org tab, verify if ticket is shown
{
:execute => 'search_user',
:term => 'Braun',
},
{
:execute => 'match',
:css => '.active .profile-window',
:value => 'user profile check ' + message,
:match_result => true,
},
],
},
]
browser_signle_test_with_login(tests, { :username => 'master@example.com' })
end
end

View file

@ -0,0 +1,92 @@
# encoding: utf-8
require 'test_helper'
class TicketRefObjectTouchTest < ActiveSupport::TestCase
# create base
groups = Group.where( :name => 'Users' )
roles = Role.where( :name => 'Agent' )
agent1 = User.create_or_update(
:login => 'ticket-ref-object-update-agent1@example.com',
:firstname => 'Notification',
:lastname => 'Agent1',
:email => 'ticket-ref-object-update-agent1@example.com',
:password => 'agentpw',
:active => true,
:roles => roles,
:groups => groups,
:updated_at => '2015-02-05 16:37:00',
:updated_by_id => 1,
:created_by_id => 1,
)
roles = Role.where( :name => 'Customer' )
customer1 = User.create_or_update(
:login => 'ticket-ref-object-update-customer1@example.com',
:firstname => 'Notification',
:lastname => 'Agent1',
:email => 'ticket-ref-object-update-customer1@example.com',
:password => 'customerpw',
:active => true,
:roles => roles,
:updated_at => '2015-02-05 16:37:00',
:updated_by_id => 1,
:created_by_id => 1,
)
organization1 = Organization.create_if_not_exists(
:name => 'Ref Object Update Org',
:updated_at => '2015-02-05 16:37:00',
:updated_by_id => 1,
:created_by_id => 1,
)
test 'check if customer and organization has been updated' do
ticket = Ticket.create(
:title => "some title\n äöüß",
:group => Group.lookup( :name => 'Users'),
:customer_id => customer1.id,
:owner_id => agent1.id,
:state => Ticket::State.lookup( :name => 'new' ),
:priority => Ticket::Priority.lookup( :name => '2 normal' ),
:updated_by_id => 1,
:created_by_id => 1,
)
assert( ticket, "ticket created" )
# check if customer and organization has been touched
customer1 = User.find(customer1.id)
if customer1.updated_at > 2.second.ago
assert( true, "customer1.updated_at has been updated" )
else
assert( false, "customer1.updated_at has not been updated" )
end
organization1 = Organization.find(organization1.id)
if organization1.updated_at > 2.second.ago
assert( true, "organization1.updated_at has been updated" )
else
assert( false, "organization1.updated_at has not been updated" )
end
sleep 5
delete = ticket.destroy
assert( delete, "ticket destroy" )
# check if customer and organization has been touched
customer1 = User.find(customer1.id)
if customer1.updated_at > 2.second.ago
assert( true, "customer1.updated_at has been updated" )
else
assert( false, "customer1.updated_at has not been updated" )
end
organization1 = Organization.find(organization1.id)
if organization1.updated_at > 2.second.ago
assert( true, "organization1.updated_at has been updated" )
else
assert( false, "organization1.updated_at has not been updated" )
end
end
end