Merge branch 'develop' into add-integer-and-tree-select-in-conditions

This commit is contained in:
Umar Sheikh 2017-12-18 15:37:23 +05:00
commit b7a33ea0e3
18 changed files with 525 additions and 145 deletions

View file

@ -35,7 +35,7 @@ class App.CustomerChat extends App.Controller
active_agent_ids: [] active_agent_ids: []
@render() @render()
@on 'layout-has-changed', @propagateLayoutChange @on('layout-has-changed', @propagateLayoutChange)
# update navbar on new status # update navbar on new status
@bind('chat_status_agent', (data) => @bind('chat_status_agent', (data) =>
@ -163,6 +163,11 @@ class App.CustomerChat extends App.Controller
@title 'Customer Chat', true @title 'Customer Chat', true
@navupdate '#customer_chat' @navupdate '#customer_chat'
if params.session_id && App.ChatSession.exists(params.session_id)
session = App.ChatSession.find(params.session_id)
@addChat(session)
@navigate '#customer_chat'
active: (state) => active: (state) =>
return @shown if state is undefined return @shown if state is undefined
@shown = state @shown = state
@ -264,10 +269,11 @@ class App.CustomerChat extends App.Controller
addChat: (session) -> addChat: (session) ->
return if @chatWindows[session.session_id] return if @chatWindows[session.session_id]
chat = new ChatWindow chat = new ChatWindow(
session: session session: session
removeCallback: @removeChat removeCallback: @removeChat
messageCallback: @updateNavMenu messageCallback: @updateNavMenu
)
@workspace.append chat.el @workspace.append chat.el
chat.render() chat.render()
@ -289,7 +295,7 @@ class App.CustomerChat extends App.Controller
propagateLayoutChange: (event) => propagateLayoutChange: (event) =>
# adjust scroll position on layoutChange # adjust scroll position on layoutChange
for session_id, chat of @chatWindows for session_id, chat of @chatWindows
chat.trigger 'layout-changed' chat.trigger('layout-changed')
acceptChat: => acceptChat: =>
return if @windowCount() >= @maxChatWindows return if @windowCount() >= @maxChatWindows
@ -324,19 +330,6 @@ class App.CustomerChat extends App.Controller
currentPosition: => currentPosition: =>
@$('.main').scrollTop() @$('.main').scrollTop()
class CustomerChatRouter extends App.ControllerPermanent
requiredPermission: 'chat.agent'
constructor: (params) ->
super
App.TaskManager.execute(
key: 'CustomerChat'
controller: 'CustomerChat'
params: {}
show: true
persistent: true
)
class ChatWindow extends App.Controller class ChatWindow extends App.Controller
className: 'chat-window' className: 'chat-window'
@ -348,6 +341,8 @@ class ChatWindow extends App.Controller
'click .js-close': 'close' 'click .js-close': 'close'
'click .js-disconnect': 'disconnect' 'click .js-disconnect': 'disconnect'
'click .js-scrollHint': 'onScrollHintClick' 'click .js-scrollHint': 'onScrollHintClick'
'click .js-info': 'toggleMeta'
'submit .js-metaForm': 'sendMetaForm'
elements: elements:
'.js-customerChatInput': 'input' '.js-customerChatInput': 'input'
@ -355,8 +350,11 @@ class ChatWindow extends App.Controller
'.js-close': 'closeButton' '.js-close': 'closeButton'
'.js-disconnect': 'disconnectButton' '.js-disconnect': 'disconnectButton'
'.js-body': 'body' '.js-body': 'body'
'.js-meta': 'meta'
'.js-name': 'metaName'
'.js-scrollHolder': 'scrollHolder' '.js-scrollHolder': 'scrollHolder'
'.js-scrollHint': 'scrollHint' '.js-scrollHint': 'scrollHint'
'.js-metaForm': 'metaForm'
sounds: sounds:
message: new Audio('assets/sounds/chat_message.mp3') message: new Audio('assets/sounds/chat_message.mp3')
@ -374,9 +372,11 @@ class ChatWindow extends App.Controller
@scrollSnapTolerance = 10 # pixels @scrollSnapTolerance = 10 # pixels
@chat = App.Chat.find(@session.chat_id) @chat = App.Chat.find(@session.chat_id)
@name = "#{@chat.displayName()} ##{@session.id}" @name = @chat.displayName()
if @session && !_.isEmpty(@session.name)
@name = @session.name
@on 'layout-change', @onLayoutChange @on('layout-change', @onLayoutChange)
@bind('chat_session_typing', (data) => @bind('chat_session_typing', (data) =>
return if data.session_id isnt @session.session_id return if data.session_id isnt @session.session_id
@ -413,12 +413,45 @@ class ChatWindow extends App.Controller
onLayoutChange: => onLayoutChange: =>
@scrollToBottom() @scrollToBottom()
render: -> toggleMeta: =>
@html App.view('customer_chat/chat_window') if @meta.hasClass('hidden')
name: @name @showMeta()
else
@hideMeta()
@el.one 'transitionend', @onTransitionend hideMeta: =>
@scrollHolder.scroll @detectScrolledtoBottom @body.removeClass('hidden')
@meta.addClass('hidden')
@sendMetaForm()
showMeta: =>
@body.addClass('hidden')
@meta.removeClass('hidden')
sendMetaForm: (e) =>
if e
e.preventDefault()
params = @formParam(@metaForm)
App.WebSocket.send(
event:'chat_session_update'
data:
session_id: @session.session_id
name: params.name
tags: params.tags
)
if !_.isEmpty(params.name)
@metaName.text(params.name)
render: ->
@html App.view('customer_chat/chat_window')(
name: @name
session: @session
)
@el.one('transitionend', @onTransitionend)
@scrollHolder.scroll(@detectScrolledtoBottom)
# force repaint # force repaint
@el.prop('offsetHeight') @el.prop('offsetHeight')
@ -426,18 +459,24 @@ class ChatWindow extends App.Controller
# @addMessage 'Hello. My name is Roger, how can I help you?', 'agent' # @addMessage 'Hello. My name is Roger, how can I help you?', 'agent'
if @session if @session
# set chat to offline if state is already closed
activeChat = true
if @session.state is 'closed'
activeChat = false
if @session && @session.preferences && @session.preferences.url if @session && @session.preferences && @session.preferences.url
@addNoticeMessage(@session.preferences.url) @addNoticeMessage(@session.preferences.url, undefined, activeChat)
if @session.messages if @session.messages
for message in @session.messages for message in @session.messages
if message.created_by_id if message.created_by_id
@addMessage message.content, 'agent' @addMessage(message.content, 'agent', false, activeChat)
else else
@addMessage message.content, 'customer' @addMessage(message.content, 'customer', false, activeChat)
# send init reply # send init reply
if !@session.messages || _.isEmpty(@session.messages) if activeChat && _.isEmpty(@session.messages)
preferences = @Session.get('preferences') preferences = @Session.get('preferences')
if preferences.chat && preferences.chat.phrase if preferences.chat && preferences.chat.phrase
phrases = preferences.chat.phrase[@session.chat_id] phrases = preferences.chat.phrase[@session.chat_id]
@ -447,20 +486,9 @@ class ChatWindow extends App.Controller
@input.html(phrase) @input.html(phrase)
@sendMessage(1600) @sendMessage(1600)
@$('.js-info').popover( # set chat to offline if state is already closed
trigger: 'hover' if !activeChat
html: true @goOffline()
animation: false
delay: 0
placement: 'bottom'
container: 'body' # place in body do prevent it from animating
title: ->
App.i18n.translateContent('Details')
content: =>
App.view('customer_chat/chat_window_info')(
session: @session
)
)
# show text module UI # show text module UI
new App.WidgetTextModule( new App.WidgetTextModule(
@ -470,6 +498,18 @@ class ChatWindow extends App.Controller
config: App.Config.all() config: App.Config.all()
) )
configureAttributesOutbound = [
{ name: 'name', display: 'Name', tag: 'input', null: true, },
{ name: 'tags', display: 'Tags', tag: 'tag', null: true, },
]
new App.ControllerForm(
el: @$('.js-metaForm')
model:
configure_attributes: configureAttributesOutbound
className: ''
params: @session
)
focus: => focus: =>
@input.focus() @input.focus()
@ -498,7 +538,8 @@ class ChatWindow extends App.Controller
@goOffline() @goOffline()
close: => close: =>
@el.one 'transitionend', { callback: @release }, @onTransitionend @sendMetaForm()
@el.one('transitionend', { callback: @release }, @onTransitionend)
@el.removeClass('is-open') @el.removeClass('is-open')
if @removeCallback if @removeCallback
@removeCallback(@session.session_id) @removeCallback(@session.session_id)
@ -577,7 +618,8 @@ class ChatWindow extends App.Controller
) )
@delay(send, delay) @delay(send, delay)
@addMessage content, 'agent' @hideMeta()
@addMessage(content, 'agent')
@input.html('') @input.html('')
updateModified: (state) => updateModified: (state) =>
@ -614,18 +656,19 @@ class ChatWindow extends App.Controller
@messageCallback(@session.session_id) @messageCallback(@session.session_id)
@unreadMessagesCounter = 0 @unreadMessagesCounter = 0
addMessage: (message, sender, isNew) => addMessage: (message, sender, isNew, useMaybeAddTimestamp = true) =>
@maybeAddTimestamp() @maybeAddTimestamp() if useMaybeAddTimestamp
@lastAddedType = sender @lastAddedType = sender
@body.append App.view('customer_chat/chat_message') @body.append App.view('customer_chat/chat_message')(
message: message message: message
sender: sender sender: sender
isNew: isNew isNew: isNew
timestamp: Date.now() timestamp: Date.now()
)
@scrollToBottom showHint: true @scrollToBottom(showHint: true)
showWritingLoader: => showWritingLoader: =>
if !@isTyping if !@isTyping
@ -667,33 +710,37 @@ class ChatWindow extends App.Controller
@lastAddedType = 'timestamp' @lastAddedType = 'timestamp'
addTimestamp: (label, time) => addTimestamp: (label, time) =>
@body.append App.view('customer_chat/chat_timestamp') @body.append App.view('customer_chat/chat_timestamp')(
label: label label: label
time: time time: time
)
updateLastTimestamp: (label, time) -> updateLastTimestamp: (label, time) ->
@body @body
.find('.js-timestamp') .find('.js-timestamp')
.last() .last()
.replaceWith App.view('customer_chat/chat_timestamp') .replaceWith App.view('customer_chat/chat_timestamp')(
label: label label: label
time: time time: time
)
addStatusMessage: (message, args) -> addStatusMessage: (message, args, useMaybeAddTimestamp = true) ->
@maybeAddTimestamp() @maybeAddTimestamp() if useMaybeAddTimestamp
@body.append App.view('customer_chat/chat_status_message') @body.append App.view('customer_chat/chat_status_message')(
message: message message: message
args: args args: args
)
@scrollToBottom() @scrollToBottom()
addNoticeMessage: (message, args) -> addNoticeMessage: (message, args, useMaybeAddTimestamp = true) ->
@maybeAddTimestamp() @maybeAddTimestamp() if useMaybeAddTimestamp
@body.append App.view('customer_chat/chat_notice_message') @body.append App.view('customer_chat/chat_notice_message')(
message: message message: message
args: args args: args
)
@scrollToBottom() @scrollToBottom()
@ -784,6 +831,24 @@ class Setting extends App.ControllerModal
msg: App.i18n.translateContent(data.message) msg: App.i18n.translateContent(data.message)
) )
class CustomerChatRouter extends App.ControllerPermanent
requiredPermission: 'chat.agent'
constructor: (params) ->
super
# cleanup params
clean_params =
session_id: params.session_id
App.TaskManager.execute(
key: 'CustomerChat'
controller: 'CustomerChat'
params: clean_params
show: true
persistent: true
)
App.Config.set('customer_chat', CustomerChatRouter, 'Routes') App.Config.set('customer_chat', CustomerChatRouter, 'Routes')
App.Config.set('customer_chat/session/:session_id', CustomerChatRouter, 'Routes')
App.Config.set('CustomerChat', { controller: 'CustomerChat', permission: ['chat.agent'] }, 'permanentTask') App.Config.set('CustomerChat', { controller: 'CustomerChat', permission: ['chat.agent'] }, 'permanentTask')
App.Config.set('CustomerChat', { prio: 1200, parent: '', name: 'Customer Chat', target: '#customer_chat', key: 'CustomerChat', shown: false, permission: ['chat.agent'], class: 'chat' }, 'NavBar') App.Config.set('CustomerChat', { prio: 1200, parent: '', name: 'Customer Chat', target: '#customer_chat', key: 'CustomerChat', shown: false, permission: ['chat.agent'], class: 'chat' }, 'NavBar')

View file

@ -79,6 +79,7 @@ class App.Search extends App.Controller
@tabs = [] @tabs = []
for model in App.Config.get('models_searchable') for model in App.Config.get('models_searchable')
model = model.replace(/::/, '')
tab = tab =
name: model name: model
model: model model: model

View file

@ -0,0 +1,32 @@
class App.ChatSession extends App.Model
@configure 'ChatSession', 'name', 'note'
@extend Spine.Model.Ajax
@url: @apiPath + '/chat_sessions'
@configure_attributes = [
{ name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, 'null': false }
{ name: 'state', display: 'State', readonly: 1 }
{ name: 'created_by_id', display: 'Created by', relation: 'User', readonly: 1 }
{ name: 'created_at', display: 'Created', tag: 'datetime', readonly: 1 }
{ name: 'updated_by_id', display: 'Updated by', relation: 'User', readonly: 1 }
{ name: 'updated_at', display: 'Updated', tag: 'datetime', readonly: 1 }
]
@configure_overview = [
'name',
'state',
'created_at',
]
uiUrl: ->
"#customer_chat/session/#{@id}"
searchResultAttributes: ->
displayName = ''
if !_.isEmpty(@name)
displayName = @displayName()
display: "##{@id} #{displayName}"
id: @id
class: 'chat_session chat_session-popover'
url: @uiUrl()
icon: 'chat'

View file

@ -7,9 +7,7 @@
</div> </div>
</div> </div>
<div class="chat-name"> <div class="chat-name">
<%= @name %> <div class="status-badge js-info"> <span class="js-name js-info u-clickable"><%= @name %><span> #<%= @session.id %>
<div class="info-badge"><%- @Icon('info') %></div>
</div>
</div> </div>
<div class="chat-disconnect js-disconnect"> <div class="chat-disconnect js-disconnect">
<div class="btn btn--action btn--small"><%- @T('disconnect') %></div> <div class="btn btn--action btn--small"><%- @T('disconnect') %></div>
@ -24,6 +22,25 @@
</div> </div>
<div class="chat-body-holder js-scrollHolder"> <div class="chat-body-holder js-scrollHolder">
<div class="chat-body js-body"></div> <div class="chat-body js-body"></div>
<div class="chat-body js-meta hidden">
<% if @session: %>
<ul>
<li><%- @T('Created at') %>: <%- @Ttimestamp(@session.created_at) %></li>
<% if @session && @session.preferences: %>
<% if @session.preferences.geo_ip: %>
<li>GeoIP: <%= @session.preferences.geo_ip.country_name %> <%= @session.preferences.geo_ip.city_name %></li>
<% end %>
<% if @session.preferences.remote_ip: %>
<li>IP: <%= @session.preferences.remote_ip %></li>
<% end %>
<% if @session.preferences.dns_name: %>
<li>DNS: <%= @session.preferences.dns_name %></li>
<% end %>
<% end %>
</ul>
<% end %>
<form class="js-metaForm" style="max-width: 200px; width: 100%;"></form>
</div>
</div> </div>
<div class="chat-controls"> <div class="chat-controls">
<div class="chat-input"> <div class="chat-input">

View file

@ -1,17 +0,0 @@
<hr>
<ul>
<% if @session: %>
<li><%- @T('Created at') %>: <%- @Ttimestamp(@session.created_at) %>
<% end %>
<% if @session && @session.preferences: %>
<% if @session.preferences.geo_ip: %>
<li>GeoIP: <%= @session.preferences.geo_ip.country_name %> <%= @session.preferences.geo_ip.city_name %>
<% end %>
<% if @session.preferences.remote_ip: %>
<li>IP: <%= @session.preferences.remote_ip %>
<% end %>
<% if @session.preferences.dns_name: %>
<li>DNS: <%= @session.preferences.dns_name %>
<% end %>
<% end %>
</ul>

View file

@ -33,9 +33,10 @@ class SearchController < ApplicationController
objects_in_order = [] objects_in_order = []
objects_in_order_hash = {} objects_in_order_hash = {}
objects.each do |object| objects.each do |object|
preferences = object.constantize.search_preferences(current_user) local_class = object.constantize
preferences = local_class.search_preferences(current_user)
next if !preferences next if !preferences
objects_in_order_hash[preferences[:prio]] = object objects_in_order_hash[preferences[:prio]] = local_class
end end
objects_in_order_hash.keys.sort.reverse_each do |prio| objects_in_order_hash.keys.sort.reverse_each do |prio|
objects_in_order.push objects_in_order_hash[prio] objects_in_order.push objects_in_order_hash[prio]
@ -64,16 +65,18 @@ class SearchController < ApplicationController
items = SearchIndexBackend.search(query, limit, objects_with_direct_search_index) items = SearchIndexBackend.search(query, limit, objects_with_direct_search_index)
items.each do |item| items.each do |item|
require item[:type].to_filename require item[:type].to_filename
record = Kernel.const_get(item[:type]).lookup(id: item[:id]) local_class = Kernel.const_get(item[:type])
record = local_class.lookup(id: item[:id])
next if !record next if !record
assets = record.assets(assets) assets = record.assets(assets)
item[:type] = local_class.to_app_model.to_s
result.push item result.push item
end end
end end
# e. g. do ticket query by Ticket class to handle ticket permissions # e. g. do ticket query by Ticket class to handle ticket permissions
objects_without_direct_search_index.each do |object| objects_without_direct_search_index.each do |object|
object_result = search_generic_backend(object, query, limit, current_user, assets) object_result = search_generic_backend(object.constantize, query, limit, current_user, assets)
if object_result.present? if object_result.present?
result = result.concat(object_result) result = result.concat(object_result)
end end
@ -83,7 +86,7 @@ class SearchController < ApplicationController
result_in_order = [] result_in_order = []
objects_in_order.each do |object| objects_in_order.each do |object|
result.each do |item| result.each do |item|
next if item[:type] != object next if item[:type] != object.to_app_model.to_s
item[:id] = item[:id].to_i item[:id] = item[:id].to_i
result_in_order.push item result_in_order.push item
end end
@ -110,7 +113,7 @@ class SearchController < ApplicationController
private private
def search_generic_backend(object, query, limit, current_user, assets) def search_generic_backend(object, query, limit, current_user, assets)
found_objects = object.constantize.search( found_objects = object.search(
query: query, query: query,
limit: limit, limit: limit,
current_user: current_user, current_user: current_user,
@ -119,7 +122,7 @@ class SearchController < ApplicationController
found_objects.each do |found_object| found_objects.each do |found_object|
item = { item = {
id: found_object.id, id: found_object.id,
type: found_object.class.to_s type: found_object.class.to_app_model.to_s
} }
result.push item result.push item
assets = found_object.assets(assets) assets = found_object.assets(assets)

View file

@ -1,4 +1,15 @@
class Chat::Session < ApplicationModel class Chat::Session < ApplicationModel
include HasSearchIndexBackend
include HasTags
extend Chat::Session::Search
load 'chat/session/search_index.rb'
include Chat::Session::SearchIndex
load 'chat/session/assets.rb'
include Chat::Session::Assets
has_many :messages, class_name: 'Chat::Message', foreign_key: 'chat_session_id'
before_create :generate_session_id before_create :generate_session_id
store :preferences store :preferences

View file

@ -0,0 +1,56 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
module Chat::Session::Assets
=begin
get all assets / related models for this chat
chat = Chat::Session.find(123)
result = Chat::Session.assets(assets_if_exists)
returns
result = {
users: {
123: user_model_123,
1234: user_model_1234,
},
chat_sessions: [ chat_session_model1 ]
}
=end
def assets(data)
app_model_chat_session = Chat::Session.to_app_model
app_model_chat = Chat.to_app_model
app_model_user = User.to_app_model
data[ app_model_chat_session ] ||= {}
if !data[ app_model_chat_session ][ id ]
data[ app_model_chat_session ][ id ] = attributes_with_association_ids
data[ app_model_chat_session ][ id ]['messages'] = []
messages.each do |message|
data[ app_model_chat_session ][ id ]['messages'].push message.attributes
end
data[ app_model_chat_session ][ id ]['tags'] = tag_list
end
if !data[ app_model_chat ] || !data[ app_model_chat ][ chat_id ]
chat = Chat.lookup(id: chat_id)
if chat
data = chat.assets(data)
end
end
%w[created_by_id updated_by_id].each do |local_user_id|
next if !self[ local_user_id ]
next if data[ app_model_user ] && data[ app_model_user ][ self[ local_user_id ] ]
user = User.lookup(id: self[ local_user_id ])
next if !user
data = user.assets(data)
end
data
end
end

View file

@ -0,0 +1,80 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
class Chat::Session
module Search
=begin
search organizations preferences
result = Chat::Session.search_preferences(user_model)
returns if user has permissions to search
result = {
prio: 1000,
direct_search_index: true
}
returns if user has no permissions to search
result = false
=end
def search_preferences(current_user)
return false if Setting.get('chat') != true || !current_user.permissions?('chat.agent')
{
prio: 900,
direct_search_index: true,
}
end
=begin
search organizations
result = Chat::Session.search(
current_user: User.find(123),
query: 'search something',
limit: 15,
)
returns
result = [organization_model1, organization_model2]
=end
def search(params)
# get params
query = params[:query]
limit = params[:limit] || 10
current_user = params[:current_user]
# enable search only for agents and admins
return [] if !search_preferences(current_user)
# try search index backend
if SearchIndexBackend.enabled?
items = SearchIndexBackend.search(query, limit, 'Chat::Session')
chat_sessions = []
items.each do |item|
chat_session = Chat::Session.lookup(id: item[:id])
next if !chat_session
chat_sessions.push chat_session
end
return chat_sessions
end
# fallback do sql query
# - stip out * we already search for *query* -
query.delete! '*'
chat_sessions = Chat::Session.where(
'name LIKE ?', "%#{query}%"
).order('name').limit(limit).to_a
chat_sessions
end
end
end

View file

@ -0,0 +1,36 @@
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
module Chat::Session::SearchIndex
=begin
lookup name of ref. objects
chat_session = Chat::Session.find(123)
result = chat_session.search_index_attribute_lookup
returns
attributes # object with lookup data
=end
def search_index_attribute_lookup
attributes = super
return if !attributes
attributes[:tag] = tag_list
messages = Chat::Message.where(chat_session_id: id)
attributes['messages'] = []
messages.each do |message|
# lookup attributes of ref. objects (normally name and note)
message_attributes = message.search_index_attribute_lookup
attributes['messages'].push message_attributes
end
attributes
end
end

View file

@ -30,7 +30,7 @@ returns
} }
selector = params[:selector].clone selector = params[:selector].clone
if params[:params] && params[:params][:selector] if params[:params].present? && params[:params][:selector].present?
selector = selector.merge(params[:params][:selector]) selector = selector.merge(params[:params][:selector])
end end

View file

@ -74,6 +74,7 @@ update processors
) )
Rails.logger.info "# #{response.code}" Rails.logger.info "# #{response.code}"
next if response.success? next if response.success?
next if response.code.to_s == '404'
raise "Unable to process DELETE at #{url}\n#{response.inspect}" raise "Unable to process DELETE at #{url}\n#{response.inspect}"
end end
Rails.logger.info "# curl -X PUT \"#{url}\" \\" Rails.logger.info "# curl -X PUT \"#{url}\" \\"
@ -133,7 +134,7 @@ create/update/delete index
def self.index(data) def self.index(data)
url = build_url(data[:name]) url = build_url(data[:name])
return if !url return if url.blank?
if data[:action] && data[:action] == 'delete' if data[:action] && data[:action] == 'delete'
return SearchIndexBackend.remove(data[:name]) return SearchIndexBackend.remove(data[:name])
@ -169,7 +170,7 @@ add new object to search index
def self.add(type, data) def self.add(type, data)
url = build_url(type, data['id']) url = build_url(type, data['id'])
return if !url return if url.blank?
Rails.logger.info "# curl -X POST \"#{url}\" \\" Rails.logger.info "# curl -X POST \"#{url}\" \\"
Rails.logger.debug "-d '#{data.to_json}'" Rails.logger.debug "-d '#{data.to_json}'"
@ -202,7 +203,7 @@ remove whole data from index
def self.remove(type, o_id = nil) def self.remove(type, o_id = nil)
url = build_url(type, o_id) url = build_url(type, o_id)
return if !url return if url.blank?
Rails.logger.info "# curl -X DELETE \"#{url}\"" Rails.logger.info "# curl -X DELETE \"#{url}\""
@ -217,7 +218,8 @@ remove whole data from index
) )
Rails.logger.info "# #{response.code}" Rails.logger.info "# #{response.code}"
return true if response.success? return true if response.success?
#Rails.logger.info "NOTICE: can't delete index #{url}: " + response.inspect return true if response.code.to_s == '400'
Rails.logger.info "NOTICE: can't delete index #{url}: " + response.inspect
false false
end end
@ -247,7 +249,7 @@ return search result
=end =end
def self.search(query, limit = 10, index = nil, query_extention = {}) def self.search(query, limit = 10, index = nil, query_extention = {})
return [] if !query return [] if query.blank?
if index.class == Array if index.class == Array
ids = [] ids = []
index.each do |local_index| index.each do |local_index|
@ -260,10 +262,10 @@ return search result
end end
def self.search_by_index(query, limit = 10, index = nil, query_extention = {}) def self.search_by_index(query, limit = 10, index = nil, query_extention = {})
return [] if !query return [] if query.blank?
url = build_url url = build_url
return if !url return if url.blank?
url += if index url += if index
if index.class == Array if index.class == Array
"/#{index.join(',')}/_search" "/#{index.join(',')}/_search"
@ -287,12 +289,8 @@ return search result
] ]
data['query'] = query_extention || {} data['query'] = query_extention || {}
if !data['query']['bool'] data['query']['bool'] ||= {}
data['query']['bool'] = {} data['query']['bool']['must'] ||= []
end
if !data['query']['bool']['must']
data['query']['bool']['must'] = []
end
# add * on simple query like "somephrase23" or "attribute: somephrase23" # add * on simple query like "somephrase23" or "attribute: somephrase23"
if query.present? if query.present?
@ -391,7 +389,7 @@ get count of tickets and tickets which match on selector
raise 'no selectors given' if !selectors raise 'no selectors given' if !selectors
url = build_url url = build_url
return if !url return if url.blank?
url += if index url += if index
if index.class == Array if index.class == Array
"/#{index.join(',')}/_search" "/#{index.join(',')}/_search"
@ -425,7 +423,7 @@ get count of tickets and tickets which match on selector
end end
Rails.logger.debug response.data.to_json Rails.logger.debug response.data.to_json
if !aggs_interval || !aggs_interval[:interval] if aggs_interval.blank? || aggs_interval[:interval].blank?
ticket_ids = [] ticket_ids = []
response.data['hits']['hits'].each do |item| response.data['hits']['hits'].each do |item|
ticket_ids.push item['_id'] ticket_ids.push item['_id']
@ -471,8 +469,8 @@ get count of tickets and tickets which match on selector
} }
# add aggs to filter # add aggs to filter
if aggs_interval if aggs_interval.present?
if aggs_interval[:interval] if aggs_interval[:interval].present?
data[:size] = 0 data[:size] = 0
data[:aggs] = { data[:aggs] = {
time_buckets: { time_buckets: {
@ -492,9 +490,7 @@ get count of tickets and tickets which match on selector
query_must.push r query_must.push r
end end
if !data[:query][:bool] data[:query][:bool] ||= {}
data[:query][:bool] = {}
end
if query_must.present? if query_must.present?
data[:query][:bool][:must] = query_must data[:query][:bool][:must] = query_must
@ -504,7 +500,7 @@ get count of tickets and tickets which match on selector
end end
# add sort # add sort
if aggs_interval && aggs_interval[:field] && !aggs_interval[:interval] if aggs_interval.present? && aggs_interval[:field].present? && aggs_interval[:interval].blank?
sort = [] sort = []
sort[0] = {} sort[0] = {}
sort[0][aggs_interval[:field]] = { sort[0][aggs_interval[:field]] = {

View file

@ -49,7 +49,7 @@ class Sessions::Event::Base
true true
end end
def permission_check(key, event) def current_user_id
if !@session if !@session
error = { error = {
event: "#{event}_error", event: "#{event}_error",
@ -60,7 +60,7 @@ class Sessions::Event::Base
Sessions.send(@client_id, error) Sessions.send(@client_id, error)
return return
end end
if !@session['id'] if @session['id'].blank?
error = { error = {
event: "#{event}_error", event: "#{event}_error",
data: { data: {
@ -70,7 +70,13 @@ class Sessions::Event::Base
Sessions.send(@client_id, error) Sessions.send(@client_id, error)
return return
end end
user = User.lookup(id: @session['id']) @session['id']
end
def current_user
user_id = current_user_id
return if !user_id
user = User.find_by(id: user_id)
if !user if !user
error = { error = {
event: "#{event}_error", event: "#{event}_error",
@ -81,6 +87,12 @@ class Sessions::Event::Base
Sessions.send(@client_id, error) Sessions.send(@client_id, error)
return return
end end
user
end
def permission_check(key, event)
user = current_user
return if !user
if !user.permissions?(key) if !user.permissions?(key)
error = { error = {
event: "#{event}_error", event: "#{event}_error",

View file

@ -0,0 +1,35 @@
class Sessions::Event::ChatSessionUpdate < Sessions::Event::ChatBase
def run
return super if super
return if !check_chat_session_exists
return if !permission_check('chat.agent', 'chat')
chat_session = current_chat_session
if @payload['data']['name'] != chat_session.name
chat_session.name = @payload['data']['name']
chat_session.save!
end
if @payload['data']['tags']
new_tags = @payload['data']['tags'].split(',')
new_tags.each(&:strip!)
tags = chat_session.tag_list
new_tags.each do |new_tag|
next if new_tag.blank?
next if tags.include?(new_tag)
chat_session.tag_add(new_tag, current_user_id)
end
tags.each do |tag|
next if new_tags.include?(tag)
chat_session.tag_remove(tag, current_user_id)
end
end
nil
end
end

View file

@ -5,15 +5,17 @@ namespace :searchindex do
task :drop, [:opts] => :environment do |_t, _args| task :drop, [:opts] => :environment do |_t, _args|
# drop indexes # drop indexes
puts 'drop indexes...' print 'drop indexes...'
SearchIndexBackend.index( SearchIndexBackend.index(
action: 'delete', action: 'delete',
) )
puts 'done'
Rake::Task['searchindex:drop_pipeline'].execute
end end
task :create, [:opts] => :environment do |_t, _args| task :create, [:opts] => :environment do |_t, _args|
puts 'create indexes...' print 'create indexes...'
# es with mapper-attachments plugin # es with mapper-attachments plugin
info = SearchIndexBackend.info info = SearchIndexBackend.info
@ -45,6 +47,7 @@ namespace :searchindex do
} }
} }
) )
puts 'done'
Setting.set('es_pipeline', '') Setting.set('es_pipeline', '')
# es with ingest-attachment plugin # es with ingest-attachment plugin
@ -61,44 +64,86 @@ namespace :searchindex do
} }
} }
) )
puts 'done'
end
# update processors Rake::Task['searchindex:create_pipeline'].execute
pipeline = 'zammad-attachment' end
task :create_pipeline, [:opts] => :environment do |_t, _args|
# es with mapper-attachments plugin
info = SearchIndexBackend.info
number = nil
if info.present?
number = info['version']['number'].to_s
end
next if number.blank? || number =~ /^[2-4]\./ || number =~ /^5\.[0-5]\./
# update processors
pipeline = Setting.get('es_pipeline')
if pipeline.blank?
pipeline = "zammad#{rand(999_999_999_999)}"
Setting.set('es_pipeline', pipeline) Setting.set('es_pipeline', pipeline)
SearchIndexBackend.processors( end
"_ingest/pipeline/#{pipeline}": [ print 'create pipeline (pipeline)... '
{ SearchIndexBackend.processors(
action: 'delete', "_ingest/pipeline/#{pipeline}": [
}, {
{ action: 'delete',
action: 'create', },
description: 'Extract zammad-attachment information from arrays', {
processors: [ action: 'create',
{ description: 'Extract zammad-attachment information from arrays',
foreach: { processors: [
field: 'article', {
ignore_failure: true, foreach: {
processor: { field: 'article',
foreach: { ignore_failure: true,
field: '_ingest._value.attachment', processor: {
ignore_failure: true, foreach: {
processor: { field: '_ingest._value.attachment',
attachment: { ignore_failure: true,
target_field: '_ingest._value', processor: {
field: '_ingest._value._content', attachment: {
ignore_failure: true, target_field: '_ingest._value',
} field: '_ingest._value._content',
ignore_failure: true,
} }
} }
} }
} }
} }
] }
} ]
] }
) ]
end )
puts 'done'
end
task :drop_pipeline, [:opts] => :environment do |_t, _args|
# es with mapper-attachments plugin
info = SearchIndexBackend.info
number = nil
if info.present?
number = info['version']['number'].to_s
end
next if number.blank? || number =~ /^[2-4]\./ || number =~ /^5\.[0-5]\./
# update processors
pipeline = Setting.get('es_pipeline')
next if pipeline.blank?
print 'delete pipeline (pipeline)... '
SearchIndexBackend.processors(
"_ingest/pipeline/#{pipeline}": [
{
action: 'delete',
},
]
)
puts 'done'
end end
task :reload, [:opts] => :environment do |_t, _args| task :reload, [:opts] => :environment do |_t, _args|

View file

@ -71,8 +71,11 @@
height: 100%; height: 100%;
width: 3.4em; width: 3.4em;
text-align: center; text-align: center;
line-height: 3.5em; line-height: 3.4em;
cursor: pointer; } cursor: pointer; }
.zammad-chat-header-icon:before {
content: "";
display: inline-block; }
.zammad-chat-header-icon-open, .zammad-chat-header-icon-open,
.zammad-chat-header-icon-close { .zammad-chat-header-icon-close {
@ -107,7 +110,7 @@
font-weight: bold; } font-weight: bold; }
.zammad-chat-agent-status { .zammad-chat-agent-status {
margin: 0 1em; margin: 0.25em 1em;
display: inline-block; display: inline-block;
line-height: 2em; line-height: 2em;
padding: 0 .7em; padding: 0 .7em;

View file

@ -80,8 +80,13 @@
height: 100%; height: 100%;
width: 3.4em; width: 3.4em;
text-align: center; text-align: center;
line-height: 3.5em; line-height: 3.4em;
cursor: pointer; cursor: pointer;
&:before { // force the icon to align to center
content: "";
display: inline-block;
}
} }
.zammad-chat-header-icon-open, .zammad-chat-header-icon-open,
@ -125,7 +130,7 @@
} }
.zammad-chat-agent-status { .zammad-chat-agent-status {
margin: 0 1em; margin: 0.25em 1em;
display: inline-block; display: inline-block;
line-height: 2em; line-height: 2em;
padding: 0 .7em; padding: 0 .7em;

View file

@ -243,8 +243,8 @@ class ModelTest < ActiveSupport::TestCase
assert(searchable.include?(Ticket)) assert(searchable.include?(Ticket))
assert(searchable.include?(User)) assert(searchable.include?(User))
assert(searchable.include?(Organization)) assert(searchable.include?(Organization))
assert_equal(3, searchable.count) assert(searchable.include?(Chat::Session))
assert_equal(4, searchable.count)
end end
end end