Fixes #3302 - Representation of inactive customers and orgnizations.

This commit is contained in:
Dominik Klein 2021-07-28 16:21:23 +02:00 committed by Thorsten Eckel
parent 8a42ec4f7b
commit 7f9c477cf4
25 changed files with 248 additions and 41 deletions

View file

@ -14,10 +14,15 @@ class App.OrganizationProfile extends App.Controller
if App.Organization.exists(@organization_id)
organization = App.Organization.find(@organization_id)
icon = organization.icon()
if organization.active is false
icon = 'inactive-' + icon
meta.head = organization.displayName()
meta.title = organization.displayName()
meta.iconClass = organization.icon()
meta.iconClass = icon
meta.active = organization.active
meta
url: =>

View file

@ -15,10 +15,15 @@ class App.UserProfile extends App.Controller
if App.User.exists(@user_id)
user = App.User.find(@user_id)
icon = user.icon()
if user.active is false
icon = 'inactive-' + icon
meta.head = user.displayName()
meta.title = user.displayName()
meta.iconClass = user.icon()
meta.iconClass = icon
meta.active = user.active
meta
url: =>

View file

@ -9,7 +9,6 @@ class App.GlobalSearch extends App.Controller
search: (params) =>
query = params.query
# use cache for search result
currentTime = new Date
if @searchResultCache[query] && @searchResultCache[query].time > currentTime.setSeconds(currentTime.getSeconds() - 20)

View file

@ -20,7 +20,10 @@ class App.SingleObjectPopoverProvider extends App.PopoverProvider
buildTitleFor: (elem) ->
object = @constructor.klass.find(@objectIdFor(elem))
App.Utils.htmlEscape(@displayTitleUsing(object))
title = App.Utils.htmlEscape(@displayTitleUsing(object))
if object.active is false
title = '<span class="is-inactive">' + title + '</span>'
title
buildContentFor: (elem) ->
id = @objectIdFor(elem)

View file

@ -240,8 +240,9 @@ class App.SearchableSelect extends Spine.Controller
@invisiblePart.text('')
selectItem: (event) ->
return if !event.currentTarget.textContent
@input.val event.currentTarget.textContent.trim()
currentText = event.currentTarget.querySelector('span.searchableSelect-option-text').textContent.trim()
return if !currentText
@input.val currentText
@input.trigger('change')
@shadowInput.val event.currentTarget.getAttribute('data-value')
@shadowInput.trigger('change')
@ -351,7 +352,7 @@ class App.SearchableSelect extends Spine.Controller
event.preventDefault()
if @currentItem || !@attribute.unknown
valueName = @currentItem.text().trim()
valueName = @currentItem.children('span.searchableSelect-option-text').text().trim()
value = @currentItem.attr('data-value')
@input.val valueName
@shadowInput.val value
@ -427,7 +428,7 @@ class App.SearchableSelect extends Spine.Controller
@currentItem.addClass 'is-active'
if autocomplete
@autocomplete @currentItem.attr('data-value'), @currentItem.text().trim()
@autocomplete @currentItem.attr('data-value'), @currentItem.children('span.searchableSelect-option-text').text().trim()
highlightItem: (event) =>
@unhighlightCurrentItem()

View file

@ -1311,6 +1311,14 @@ class App.Utils
autocomplete: {
source: source
minLength: 2
create: ->
$(@).data('ui-autocomplete')._renderItem = (ul, item) ->
option_html = App.Utils.htmlEscape(item.label)
additional_class = ''
if item.inactive
option_html += '<span style="float: right;">' + App.i18n.translateContent('inactive') + '</span>'
additional_class = 'is-inactive'
return $('<li>').addClass(additional_class).append(option_html).appendTo(ul)
},
).on('tokenfield:createtoken', (e) ->
if type is 'email' && !e.attrs.value.match(/@/) || e.attrs.value.match(/\s/)

View file

@ -38,7 +38,6 @@ class App.SearchableAjaxSelect extends App.SearchableSelect
success: (data, status, xhr) =>
# cache search result
@searchResultCache[cacheKey] = data
@renderResponse(data, query)
# if delegate is given and provides getAjaxAttributes method, try to extend ajax call
@ -81,7 +80,6 @@ class App.SearchableAjaxSelect extends App.SearchableSelect
category = undefined
if result.type is 'KnowledgeBase::Answer::Translation' && result.subtitle
category = result.subtitle
if result
{
category: category
@ -103,6 +101,7 @@ class App.SearchableAjaxSelect extends App.SearchableSelect
{
name: name
value: object.id
inactive: object.active == false
}
showLoader: =>

View file

@ -41,11 +41,18 @@ Using **Organisations** you can **group** customers. This has among others two i
data
searchResultAttributes: ->
classList = ['organization', 'organization-popover' ]
icon = 'organization'
if @active is false
classList.push 'is-inactive'
icon = 'inactive-' + icon
display: "#{@displayName()}"
id: @id
class: 'organization organization-popover'
class: classList.join(' ')
url: @uiUrl()
icon: 'organization'
icon: icon
activityMessage: (item) ->
return if !item

View file

@ -156,11 +156,18 @@ class App.User extends App.Model
data
searchResultAttributes: ->
classList = ['user', 'user-popover']
icon = 'user'
if @active is false
classList.push 'is-inactive'
icon = 'inactive-' + icon
display: "#{@displayName()}"
id: @id
class: 'user user-popover'
class: classList.join(' ')
url: @uiUrl()
icon: 'user'
icon: icon
activityMessage: (item) ->
return if !item

View file

@ -1,4 +1,4 @@
<li class="recipientList-entry js-object" data-object-id="<%= @object.id %>">
<li class="recipientList-entry js-object<% if @object.active is false: %> is-inactive<% end %>" data-object-id="<%= @object.id %>">
<div class="recipientList-iconSpacer">
<%- @Icon(@icon, 'recipientList-icon') %>
</div>
@ -9,10 +9,10 @@
<%= @object.displayName() %>
<% end %>
<% if @object.organization: %>
<span class="recipientList-detail">- <%= @object.organization.displayName() %></span>
<span class="recipientList-detail<% if @object.organization.active is false: %> is-inactive<% end %>">- <%= @object.organization.displayName() %></span>
<% end %>
</div>
<% if @object.active is false: %>
<div class="recipientList-status"><%- @Ti('inactive') %></div>
<% end %>
</li>
</li>

View file

@ -1,4 +1,4 @@
<li class="recipientList-entry js-organization" data-organization-id="<%- @organization.id %>">
<li class="recipientList-entry js-organization<% if @organization.active is false: %> is-inactive<% end %>" data-organization-id="<%- @organization.id %>">
<div class="recipientList-iconSpacer">
<% if @organization.active is false: %>
<%- @Icon('inactive-organization', 'recipientList-icon') %>
@ -14,4 +14,4 @@
<div class="recipientList-status"><%- @Ti('inactive') %></div>
<% end %>
<%- @Icon('arrow-right', 'recipientList-arrow') %>
</li>
</li>

View file

@ -1,9 +1,14 @@
<li role="presentation" class="<%= @class %>" data-value="<%= @option.value %>" title="<%= @option.name %><% if @detail: %><%= @detail %><% end %>">
<% if @option.category: %><small><%= @option.category %></small><br><% end %>
<span class="searchableSelect-option-text">
<span class="searchableSelect-option-text<% if @option.inactive is true: %> is-inactive<% end %>">
<%= @option.name %><% if @detail: %><span class="dropdown-detail"><%= @detail %></span><% end %>
</span>
<% if @option.children: %>
<%- @Icon('arrow-right', 'recipientList-arrow') %>
<% end %>
<% if @option.inactive is true: %>
<span>
<%- @Ti('inactive') %>
</span>
<% end %>
</li>

View file

@ -11,4 +11,4 @@
</a>
</li>
<% end %>
<% end %>
<% end %>

View file

@ -3,11 +3,15 @@
<div class="profile-section vertical centered">
<div class="align-right profile-action js-action"></div>
<div class="profile-organizationIcon">
<%- @Icon('organization') %>
<% if @organization.active is true: %>
<%- @Icon('organization') %>
<% else: %>
<%- @Icon('inactive-organization') %>
<% end %>
</div>
<h1 class="js-name"></h1>
</div>
<div class="js-object-container"></div>
<div class="profile-section js-ticket-stats"></div>
</div>
</div>
</div>

View file

@ -5,7 +5,7 @@
<div class="popover-block">
<label><%- @T('Members') %></label>
<% for user in @object.members: %>
<div class="person"><%= user.displayName() %></div>
<div class="person<% if user.active is false: %> is-inactive<% end %>"><%= user.displayName() %></div>
<% end %>
</div>
<% end %>

View file

@ -1,5 +1,5 @@
<% if @object['organization']: %>
<div class="user-organization"><a href="<%- @object.organization.uiUrl() %>"><%= @object.organization.displayName() %></a></div>
<div class="user-organization<% if @object.organization.active is false: %> is-inactive<% end %>"><a href="<%- @object.organization.uiUrl() %>"><%= @object.organization.displayName() %></a></div>
<% end %>
<%- @V('popover/single_object_generic', object: @object, attributes: @attributes) %>

View file

@ -1,7 +1,11 @@
<div class="sidebar-block">
<div class="avatar organizationInfo-avatar size-50">
<a href="<%- @organization.uiUrl() %>">
<%- @Icon('organization') %>
<% if @organization.active is true: %>
<%- @Icon('organization') %>
<% else: %>
<%- @Icon('inactive-organization') %>
<% end %>
</a>
</div>
<h3 title="<%- @Ti( 'Name') %>"><%= @organization.displayName() %></h3>

View file

@ -11,7 +11,7 @@
<%- @Icon(@item.meta.iconClass) %>
<% end %>
</div>
<div class="nav-tab-name u-textTruncate flex"><%= @item.meta.head %></div>
<div class="nav-tab-name u-textTruncate flex<% if @item.meta.active is false: %> is-inactive<% end %>"><%= @item.meta.head %></div>
<div class="nav-tab-close js-close" title="<%- @Ti('close') %>">
<div class="nav-tab-close-inner">
<%- @Icon('diagonal-cross') %>

View file

@ -4285,6 +4285,12 @@ footer {
.navigation .nav-tab-name {
text-align: start;
&.is-inactive {
text-decoration: line-through;
opacity: .73;
}
}
.tasks-navigation .nav-tab-icon .error {
@ -4314,6 +4320,11 @@ footer {
.nav-tab.nav-tab--search {
height: 30px;
padding-top: 9px;
&.is-inactive {
text-decoration: line-through;
opacity: .73;
}
}
.nav-tab-icon {
@ -5353,6 +5364,11 @@ footer {
border: none;
background: none;
padding: 21px 17px 4px;
.is-inactive {
text-decoration: line-through;
color: #ccc;
}
}
.popover-content {
@ -5411,10 +5427,22 @@ footer {
color: #a1a4a7;
}
.popover .person {
&.is-inactive {
text-decoration: line-through;
color: #ccc;
}
}
.popover .user-organization {
@extend .u-textTruncate;
margin-bottom: 8px;
margin-top: -4px;
&.is-inactive {
text-decoration: line-through;
color: #ccc;
}
}
.popover-block {
@ -5555,6 +5583,11 @@ footer {
top: 0;
}
.user-popover.is-inactive {
text-decoration: line-through;
opacity: .73;
}
.btn.js-newTicket {
position: absolute;
right: 0;
@ -7996,6 +8029,11 @@ footer {
padding: 9px 15px;
list-style-image: none;
&.is-inactive {
text-decoration: line-through;
opacity: .73;
}
&:not(:first-child) {
box-shadow: 0 1px rgba(255,255,255,.13) inset;
}
@ -8566,6 +8604,13 @@ footer {
display: flex;
align-items: center;
@extend .u-clickable;
&.is-inactive {
.recipientList-name {
text-decoration: line-through;
opacity: .73;
}
}
}
.recipientList-entry .recipientList-iconSpacer {
@ -8620,6 +8665,10 @@ footer {
.recipientList-detail {
opacity: 0.5;
&.is-inactive > span {
text-decoration: line-through;
}
}
.recipientList-icon.plus {
@ -10354,6 +10403,11 @@ output {
& + .icon {
@include bidi-style(margin-left, 10px, margin-right, 0);
}
&.is-inactive {
opacity: .73;
text-decoration: line-through;
}
}
&.dropdown li {

View file

@ -282,7 +282,7 @@ class UsersController < ApplicationController
realname = "#{realname} <#{user.email}>"
end
a = if params[:term]
{ id: user.id, label: realname, value: user.email }
{ id: user.id, label: realname, value: user.email, inactive: !user.active }
else
{ id: user.id, label: realname, value: realname }
end

View file

@ -65,6 +65,18 @@ RSpec.describe 'User', type: :request do
)
end
let!(:customer_inactive) do
create(
:customer,
organization: organization,
login: 'rest-customer_inactive@example.com',
firstname: 'Rest',
lastname: 'CustomerInactive',
email: 'rest-customer_inactive@example.com',
active: false,
)
end
before do
configure_elasticsearch(rebuild: true)
end
@ -224,7 +236,7 @@ RSpec.describe 'User', type: :request do
get '/api/v1/users/me', params: {}, as: :json
expect(response).to have_http_status(:ok)
expect(json_response).to be_truthy
expect('rest-admin@example.com').to eq(json_response['email'])
expect(json_response['email']).to eq('rest-admin@example.com')
# index
get '/api/v1/users', params: {}, as: :json
@ -243,13 +255,13 @@ RSpec.describe 'User', type: :request do
expect(response).to have_http_status(:ok)
expect(json_response).to be_truthy
expect(Hash).to eq(json_response.class)
expect('rest-agent@example.com').to eq(json_response['email'])
expect(json_response['email']).to eq('rest-agent@example.com')
get "/api/v1/users/#{customer.id}", params: {}, as: :json
expect(response).to have_http_status(:ok)
expect(json_response).to be_truthy
expect(Hash).to eq(json_response.class)
expect('rest-customer1@example.com').to eq(json_response['email'])
expect(json_response['email']).to eq('rest-customer1@example.com')
# create user with admin role
role = Role.lookup(name: 'Admin')
@ -332,7 +344,7 @@ RSpec.describe 'User', type: :request do
get '/api/v1/users/me', params: {}, as: :json
expect(response).to have_http_status(:ok)
expect(json_response).to be_truthy
expect('rest-agent@example.com').to eq(json_response['email'])
expect(json_response['email']).to eq('rest-agent@example.com')
# index
get '/api/v1/users', params: {}, as: :json
@ -442,9 +454,15 @@ RSpec.describe 'User', type: :request do
expect(json_response[0]['id']).to eq(json_response1['id'])
expect(json_response[0]['label']).to eq("Customer#{firstname} Customer Last <new_customer_by_agent@example.com>")
expect(json_response[0]['value']).to eq('new_customer_by_agent@example.com')
expect(json_response[0]['inactive']).to eq(false)
expect(json_response[0]['role_ids']).to be_falsey
expect(json_response[0]['roles']).to be_falsey
get "/api/v1/users/search?term=#{CGI.escape('CustomerInactive')}", params: {}, as: :json
expect(response).to have_http_status(:ok)
expect(json_response).to be_a_kind_of(Array)
expect(json_response[0]['inactive']).to eq(true)
# Regression test for issue #2539 - search pagination broken in users_controller.rb
# Get the total number of users N, then search with one result per page, so there should N pages with one result each
get '/api/v1/users/search', params: { query: '*' }, as: :json
@ -494,19 +512,19 @@ RSpec.describe 'User', type: :request do
get '/api/v1/users/me', params: {}, as: :json
expect(response).to have_http_status(:ok)
expect(json_response).to be_truthy
expect('rest-customer1@example.com').to eq(json_response['email'])
expect(json_response['email']).to eq('rest-customer1@example.com')
# index
get '/api/v1/users', params: {}, as: :json
expect(response).to have_http_status(:ok)
expect(Array).to eq(json_response.class)
expect(1).to eq(json_response.length)
expect(json_response.length).to eq(1)
# show/:id
get "/api/v1/users/#{customer.id}", params: {}, as: :json
expect(response).to have_http_status(:ok)
expect(Hash).to eq(json_response.class)
expect('rest-customer1@example.com').to eq(json_response['email'])
expect(json_response['email']).to eq('rest-customer1@example.com')
get "/api/v1/users/#{customer2.id}", params: {}, as: :json
expect(response).to have_http_status(:forbidden)
@ -536,19 +554,19 @@ RSpec.describe 'User', type: :request do
get '/api/v1/users/me', params: {}, as: :json
expect(response).to have_http_status(:ok)
expect(json_response).to be_truthy
expect('rest-customer2@example.com').to eq(json_response['email'])
expect(json_response['email']).to eq('rest-customer2@example.com')
# index
get '/api/v1/users', params: {}, as: :json
expect(response).to have_http_status(:ok)
expect(Array).to eq(json_response.class)
expect(1).to eq(json_response.length)
expect(json_response.length).to eq(1)
# show/:id
get "/api/v1/users/#{customer2.id}", params: {}, as: :json
expect(response).to have_http_status(:ok)
expect(Hash).to eq(json_response.class)
expect('rest-customer2@example.com').to eq(json_response['email'])
expect(json_response['email']).to eq('rest-customer2@example.com')
get "/api/v1/users/#{customer.id}", params: {}, as: :json
expect(response).to have_http_status(:forbidden)

View file

@ -69,8 +69,25 @@ RSpec.describe 'Manage > Users', type: :system do
expect(page).to have_css('table.user-list td', text: 'NewTestUserFirstName')
end
end
describe 'select an Organization' do
before do
create(:organization, name: 'Example Inc.', active: true)
create(:organization, name: 'Inactive Inc.', active: false)
end
it 'check for inactive Organizations in Organization selection' do
visit '#manage/users'
within(:active_content) do
find('[data-type=new]').click
find('[name=organization_id] ~ .searchableSelect-main').fill_in with: '**'
expect(page).to have_css('ul.js-optionsList > li.js-option', minimum: 2)
expect(page).to have_css('ul.js-optionsList > li.js-option .is-inactive', count: 1)
end
end
end
end
end

View file

@ -16,4 +16,37 @@ RSpec.describe 'Search', type: :system, searchindex: true do
expect(page).to have_text '"Welcome"'
end
end
context 'inactive user and organizations' do
before do
create(:organization, name: 'Example Inc.', active: true)
create(:organization, name: 'Example Inactive Inc.', active: false)
create(:customer, firstname: 'Firstname', lastname: 'Active', active: true)
create(:customer, firstname: 'Firstname', lastname: 'Inactive', active: false)
configure_elasticsearch(rebuild: true)
end
it 'check that inactive organizations are marked correctly' do
fill_in id: 'global-search', with: '"Example"'
expect(page).to have_css('.nav-tab--search.organization', minimum: 2)
expect(page).to have_css('.nav-tab--search.organization.is-inactive', count: 1)
end
it 'check that inactive users are marked correctly' do
fill_in id: 'global-search', with: '"Firstname"'
expect(page).to have_css('.nav-tab--search.user', minimum: 2)
expect(page).to have_css('.nav-tab--search.user.is-inactive', count: 1)
end
it 'check that inactive users are also marked in the popover for the quick search result' do
fill_in id: 'global-search', with: '"Firstname"'
popover_on_hover(find('.nav-tab--search.user.is-inactive'))
expect(page).to have_css('.popover-title .is-inactive', count: 1)
end
end
end

View file

@ -456,4 +456,21 @@ RSpec.describe 'Ticket Create', type: :system do
expect(page).to have_no_selector(:task_with, task_key)
end
end
describe 'customer selection to check the field search' do
before do
create(:customer, active: true)
create(:customer, active: false)
end
it 'check for inactive customer in customer/organization selection' do
visit 'ticket/create'
within(:active_content) do
find('[name=customer_id] ~ .user-select.token-input').fill_in with: '**'
expect(page).to have_css('ul.recipientList > li.recipientList-entry', minimum: 2)
expect(page).to have_css('ul.recipientList > li.recipientList-entry.is-inactive', count: 1)
end
end
end
end

View file

@ -208,10 +208,14 @@ RSpec.describe 'Ticket zoom', type: :system do
context 'to inbound phone call', current_user_id: -> { agent.id }, authenticated_as: -> { agent } do
let(:agent) { create(:agent, groups: [Group.first]) }
let(:customer) { create(:agent) }
let(:customer) { create(:customer) }
let(:ticket) { create(:ticket, customer: customer, group: agent.groups.first) }
let!(:article) { create(:ticket_article, :inbound_phone, ticket: ticket) }
before do
create(:customer, active: false)
end
it 'goes to customer email' do
visit "ticket/zoom/#{ticket.id}"
@ -225,11 +229,28 @@ RSpec.describe 'Ticket zoom', type: :system do
end
end
end
it 'check active and inactive user in TO-field' do
visit "ticket/zoom/#{ticket.id}"
within :active_ticket_article, article do
click '.js-ArticleAction[data-type=emailReply]'
end
within :active_content do
within '.article-new' do
find('[name=to] ~ .ui-autocomplete-input').fill_in with: '**'
end
end
expect(page).to have_css('ul.ui-autocomplete > li.ui-menu-item', minimum: 2)
expect(page).to have_css('ul.ui-autocomplete > li.ui-menu-item.is-inactive', count: 1)
end
end
context 'to outbound phone call', current_user_id: -> { agent.id }, authenticated_as: -> { agent } do
let(:agent) { create(:agent, groups: [Group.first]) }
let(:customer) { create(:agent) }
let(:customer) { create(:customer) }
let(:ticket) { create(:ticket, customer: customer, group: agent.groups.first) }
let!(:article) { create(:ticket_article, :outbound_phone, ticket: ticket) }