Implemented issue #352 - Show other active agents in ticket detail view (collision prevention).

This commit is contained in:
Martin Edenhofer 2017-01-04 15:40:19 +01:00
parent c6346ea210
commit f9fdbe5934
20 changed files with 678 additions and 49 deletions

View file

@ -94,10 +94,10 @@ class Index extends App.ControllerSubContent
# update avatar text if needed # update avatar text if needed
if activeAvatar.text() if activeAvatar.text()
replaceAvatar.text(activeAvatar.text()) replaceAvatar.text(activeAvatar.text())
replaceAvatar.addClass('unique') replaceAvatar.addClass('avatar--unique')
else else
replaceAvatar.text('') replaceAvatar.text('')
replaceAvatar.removeClass('unique') replaceAvatar.removeClass('avatar--unique')
) )
avatar avatar

View file

@ -34,7 +34,7 @@ class Content extends App.ControllerContent
width = 300 width = 300
height = 226 height = 226
holder.addClass 'unique' holder.addClass 'avatar--unique'
rng = new Math.seedrandom(id) rng = new Math.seedrandom(id)
x = rng() * (width - size) x = rng() * (width - size)

View file

@ -47,6 +47,13 @@ class App.TicketZoomAttributeBar extends App.Controller
resetButtonShown: resetButtonShown resetButtonShown: resetButtonShown
)) ))
@setSecondaryAction(@secondaryAction, localeEl) @setSecondaryAction(@secondaryAction, localeEl)
if @permissionCheck('ticket.agent')
new App.TaskbarWatcher(
task_key: @task_key
el: localeEl.filter('.js-avatars')
)
@html localeEl @html localeEl
checkMacroChanges: => checkMacroChanges: =>

View file

@ -0,0 +1,55 @@
class App.TaskbarWatcher extends App.Controller
constructor: ->
super
@subscribeId = App.TaskManager.preferencesSubscribe(@task_key, @render)
release: =>
return if !@subscribeId
App.TaskManager.preferencesUnsubscribe(@subscribeId)
render: (preferences) =>
return if !preferences
return if !preferences.tasks
return if !@diffrence(@lastTasks, preferences.tasks)
@lastTasks = preferences.tasks
watchers = []
currentUserId = App.Session.get('id')
@el.empty()
for watcher in preferences.tasks
if watcher.user_id != currentUserId
cssClass = []
lastContact = new Date(new Date(watcher.last_contact).getTime() + 5 * 60000)
if new Date() > lastContact
cssClass.push('avatar--idle')
if watcher.changed
cssClass.push('avatar--changed')
else
cssClass.push('avatar--not-changed')
@el.append('<div class="js-avatar"></div>')
@el.append('<div class="half-spacer"></div>')
avatar = new App.WidgetAvatar(
el: @el.find('.js-avatar').last()
object_id: watcher.user_id
size: 40
cssClass: cssClass.join(' ')
)
if watcher.changed
status = $('<div class="avatar-status"></div>')
status.append App.Utils.icon('pen')
avatar.el.find('.avatar').append status
diffrence: (lastTasks, newTasks) ->
return true if !lastTasks
return true if lastTasks.length != newTasks.length
for taskPosition of lastTasks
return true if !lastTasks[taskPosition] || !newTasks[taskPosition]
return true if lastTasks[taskPosition].user_id != newTasks[taskPosition].user_id
return true if lastTasks[taskPosition].changed != newTasks[taskPosition].changed
if lastTasks[taskPosition].last_contact
lastContact = new Date(new Date(lastTasks[taskPosition].last_contact).getTime() + 5 * 60000)
return true if new Date() > lastContact
false

View file

@ -10,5 +10,5 @@ class App.WidgetAvatar extends App.ObserverController
globalRerender: false globalRerender: false
render: (user) => render: (user) =>
@html(user.avatar @size, @position, undefined, false, false, @type) @html(user.avatar(@size, @position, @cssClass, false, false, @type))
@userPopups(@position) @userPopups(@position)

View file

@ -71,6 +71,14 @@ class App.TaskManager
return if !_instance return if !_instance
_instance.showControllerHideOthers() _instance.showControllerHideOthers()
@preferencesSubscribe: (key, callback) ->
return if !_instance
_instance.preferencesSubscribe(key, callback)
@preferencesUnsubscribe: (id) ->
return if !_instance
_instance.preferencesUnsubscribe(id)
class _taskManagerSingleton extends App.Controller class _taskManagerSingleton extends App.Controller
@include App.LogInclude @include App.LogInclude
@ -83,12 +91,19 @@ class _taskManagerSingleton extends App.Controller
@offlineModus = params.offlineModus @offlineModus = params.offlineModus
@tasksInitial() @tasksInitial()
@bind('taskbar:preferences', (data) =>
@tasksPreferences[data.key] = data.preferences
@preferencesExecuteCallbacks(data.key)
)
init: -> init: ->
@domStore = {} @domStore = {}
@shownStore = {} @shownStore = {}
@workers = {} @workers = {}
@allTasksByKey = {} @allTasksByKey = {}
@tasksToUpdate = {} @tasksToUpdate = {}
@tasksPreferences = {}
@tasksPreferencesCallbacks = {}
@activeTaskHistory = [] @activeTaskHistory = []
@queue = [] @queue = []
@queueRunning = false @queueRunning = false
@ -206,13 +221,21 @@ class _taskManagerSingleton extends App.Controller
# save new task and update task collection # save new task and update task collection
ui = @ ui = @
@tasksToUpdate[params.key] = 'inCreate'
task.save( task.save(
done: -> done: ->
if ui.tasksToUpdate[params.key] is 'inCreate'
delete ui.tasksToUpdate[params.key]
ui.allTasksByKey[params.key] = @attributes() ui.allTasksByKey[params.key] = @attributes()
ui.tasksPreferences[params.key] = clone(@preferences)
ui.preferencesExecuteCallbacks(params.key)
for taskPosition of ui.allTasks for taskPosition of ui.allTasks
if ui.allTasks[taskPosition] && ui.allTasks[taskPosition]['key'] is @key if ui.allTasks[taskPosition] && ui.allTasks[taskPosition]['key'] is @key
task = @attributes() task = @attributes()
ui.allTasks[taskPosition] = task ui.allTasks[taskPosition] = task
fail: ->
if ui.tasksToUpdate[params.key] is 'inCreate'
delete ui.tasksToUpdate[params.key]
) )
# empty static content if task is shown # empty static content if task is shown
@ -491,7 +514,8 @@ class _taskManagerSingleton extends App.Controller
taskUpdate: (task, mute = false) -> taskUpdate: (task, mute = false) ->
@log 'debug', 'UPDATE task', task, mute @log 'debug', 'UPDATE task', task, mute
@tasksToUpdate[ task.key ] = 'toUpdate' return if @tasksToUpdate[task.key] is 'inCreate'
@tasksToUpdate[task.key] = 'toUpdate'
@taskUpdateTrigger() @taskUpdateTrigger()
return if mute return if mute
@touch(task.key) @touch(task.key)
@ -520,8 +544,10 @@ class _taskManagerSingleton extends App.Controller
continue if !key continue if !key
task = @get(key) task = @get(key)
continue if !task continue if !task
if @tasksToUpdate[ task.key ] is 'toUpdate' if @tasksToUpdate[task.key] is 'toUpdate'
@tasksToUpdate[ task.key ] = 'inProgress' @tasksToUpdate[task.key] = 'inProgress'
taskUpdate = App.Taskbar.findByAttribute('key', task.key)
if !taskUpdate
taskUpdate = new App.Taskbar taskUpdate = new App.Taskbar
taskUpdate.load(task) taskUpdate.load(task)
if taskUpdate.isOnline() if taskUpdate.isOnline()
@ -539,7 +565,7 @@ class _taskManagerSingleton extends App.Controller
taskDestroy: (task) -> taskDestroy: (task) ->
# check if update is still in process # check if update is still in process
if @tasksToUpdate[ task.key ] is 'inProgress' if @tasksToUpdate[task.key] is 'inProgress' || @tasksToUpdate[task.key] is 'inCreate'
App.Delay.set( App.Delay.set(
=> @taskDestroy(task) => @taskDestroy(task)
800 800
@ -550,12 +576,14 @@ class _taskManagerSingleton extends App.Controller
return return
# destroy task in backend # destroy task in backend
delete @tasksToUpdate[ task.key ] delete @tasksToUpdate[task.key]
# if task isnt already stored on backend # if task isnt already stored on backend
return if !task.id return if !task.id
App.Taskbar.destroy(task.id) App.Taskbar.destroy(task.id)
delete @tasksPreferences[task.key]
tasksAutoCleanupDelay: => tasksAutoCleanupDelay: =>
delay = => delay = =>
@tasksAutoCleanup() @tasksAutoCleanup()
@ -637,3 +665,26 @@ class _taskManagerSingleton extends App.Controller
) )
App.Event.trigger 'taskbar:ready' App.Event.trigger 'taskbar:ready'
preferencesSubscribe: (key, callback) =>
if !@tasksPreferencesCallbacks[key]
@tasksPreferencesCallbacks[key] = {}
subscribeId = "#{key}#{Math.floor(Math.random() * 999999)}"
@tasksPreferencesCallbacks[key][subscribeId] = callback
subscribeId
preferencesUnsubscribe: (id) =>
return if !@tasksPreferencesCallbacks
for key, value of @tasksPreferencesCallbacks
for subscribeId, callback of value
if subscribeId == id
delete value[subscribeId]
for key, value of @tasksPreferencesCallbacks
if _.isEmpty(value)
delete @tasksPreferencesCallbacks[key]
preferencesExecuteCallbacks: (key) =>
return if !@tasksPreferencesCallbacks[key]
return if !@tasksPreferences[key]
for subscribeId, callback of @tasksPreferencesCallbacks[key]
callback(@tasksPreferences[key])

View file

@ -1,4 +1,4 @@
<span class="avatar <%- @cssClass %> unique" style="background-position: -<%- @x %>px -<%- @y %>px;"<%- @placement %><%- @data %>> <span class="avatar <%- @cssClass %> avatar--unique" style="background-position: -<%- @x %>px -<%- @y %>px;"<%- @placement %><%- @data %>>
<%- @Icon('crown') if @vip %> <%- @Icon('crown') if @vip %>
<%= @initials %> <%= @initials %>
</span> </span>

View file

@ -32,7 +32,7 @@
</div> </div>
<div class="page-header"> <div class="page-header">
<div class="flex vertical center"> <div class="flex vertical center">
<span class="avatar unique user-popover size-50" data-id="2" style="background-position: -92.79607555375712px -106.24902447601627px;" data-original-title="" title="">NB</span> <span class="avatar avatar--unique user-popover size-50" data-id="2" style="background-position: -92.79607555375712px -106.24902447601627px;" data-original-title="" title="">NB</span>
<div class="ticket-title"> <div class="ticket-title">
<h1 contenteditable="true" class="ticket-title-update" data-placeholder="Enter Title...">Welcome to Zammad! We want to entertain you and your whole family!</h1> <h1 contenteditable="true" class="ticket-title-update" data-placeholder="Enter Title...">Welcome to Zammad! We want to entertain you and your whole family!</h1>
</div> </div>
@ -56,7 +56,7 @@
</div> </div>
</div> </div>
<div class="article-content"> <div class="article-content">
<span class="avatar unique user-popover " data-id="2" style="background-position: -96.5079185759074px -112.28590086669901px;" data-placement="left" data-original-title="" title="">NB</span> <span class="avatar avatar--unique user-popover " data-id="2" style="background-position: -96.5079185759074px -112.28590086669901px;" data-placement="left" data-original-title="" title="">NB</span>
<div class="flex bubble-gap internal-border"> <div class="flex bubble-gap internal-border">
<div class="textBubble"> <div class="textBubble">
<div class="bubble-arrow"></div> <div class="bubble-arrow"></div>
@ -155,7 +155,7 @@ The <a href="https://zammad.org" title="https://zammad.org" target="_blank">zamm
</div> </div>
</div> </div>
<div class="article-content"> <div class="article-content">
<span class="avatar unique user-popover " data-id="2" style="background-position: -96.5079185759074px -112.28590086669901px;" data-placement="left" data-original-title="" title="">NB</span> <span class="avatar avatar--unique user-popover " data-id="2" style="background-position: -96.5079185759074px -112.28590086669901px;" data-placement="left" data-original-title="" title="">NB</span>
<div class="flex bubble-gap internal-border"> <div class="flex bubble-gap internal-border">
<div class="textBubble"><div class="bubble-arrow"></div>Ich wollte mir die Lyrics von Maria herunterladen, aber ich schaff es einfach nicht, da raufzukommen. Schick mir bitte mein Passwort. <div class="textBubble"><div class="bubble-arrow"></div>Ich wollte mir die Lyrics von Maria herunterladen, aber ich schaff es einfach nicht, da raufzukommen. Schick mir bitte mein Passwort.

View file

@ -36,7 +36,7 @@
<div class="ticketZoom-header"> <div class="ticketZoom-header">
<div class="flex vertical center"> <div class="flex vertical center">
<div class="js-avatar"> <div class="js-avatar">
<span class="avatar unique size-50 user-popover" data-id="2" data-original-title="" style="background-position: -92.79607555375712px -106.24902447601627px;" title="">NB</span> <span class="avatar avatar--unique size-50 user-popover" data-id="2" data-original-title="" style="background-position: -92.79607555375712px -106.24902447601627px;" title="">NB</span>
</div> </div>
<div class="ticket-title"> <div class="ticket-title">
@ -71,7 +71,7 @@
<div class="article-content"> <div class="article-content">
<div class="js-avatar"> <div class="js-avatar">
<span class="avatar unique size-40 user-popover" data-id="2" data-original-title="" style="background-position: -96.5079185759074px -112.28590086669901px;" title="">NB</span> <span class="avatar avatar--unique size-40 user-popover" data-id="2" data-original-title="" style="background-position: -96.5079185759074px -112.28590086669901px;" title="">NB</span>
</div> </div>
<div class="bubble-gap"> <div class="bubble-gap">
@ -796,7 +796,7 @@
<div class="sidebar-content"> <div class="sidebar-content">
<div class="sidebar-block"> <div class="sidebar-block">
<span class="avatar unique userInfo-avatar size-50 user-popover" data-id="2" style="background-position: -92.79607555375712px -106.24902447601627px;">NB</span> <span class="avatar avatar--unique userInfo-avatar size-50 user-popover" data-id="2" style="background-position: -92.79607555375712px -106.24902447601627px;">NB</span>
<h3 title="Name">Nicole Braun</h3> <h3 title="Name">Nicole Braun</h3>
</div> </div>
@ -862,7 +862,7 @@
<div class="userList"> <div class="userList">
<div class="userList-entry"> <div class="userList-entry">
<span class="avatar unique size-40 user-popover" data-id="2" data-original-title="" style="background-position: -96.5079185759074px -112.28590086669901px;" title="">NB</span> <a class="userList-entry user-popover" data-id="2" data-original-title="" href="#user/profile/2" title="">Nicole Braun</a> <span class="avatar avatar--unique size-40 user-popover" data-id="2" data-original-title="" style="background-position: -96.5079185759074px -112.28590086669901px;" title="">NB</span> <a class="userList-entry user-popover" data-id="2" data-original-title="" href="#user/profile/2" title="">Nicole Braun</a>
</div> </div>
</div> </div>
</div> </div>

View file

@ -52,7 +52,7 @@
<label><%- @T('Members') %></label> <label><%- @T('Members') %></label>
<div class="profile-details horizontal wrap"> <div class="profile-details horizontal wrap">
<div class="profile-organizationMember"> <div class="profile-organizationMember">
<span class="avatar unique user-popover size-40" data-id="4" style="background-position: -97.5718417033075px -178.430732445616px;">AG</span> <span class="avatar avatar--unique user-popover size-40" data-id="4" style="background-position: -97.5718417033075px -178.430732445616px;">AG</span>
<a href="#">Doreen Kubiak</a> <a href="#">Doreen Kubiak</a>
</div> </div>
<div class="profile-organizationMember"> <div class="profile-organizationMember">
@ -60,11 +60,11 @@
<a href="#">Franz Xaver</a> <a href="#">Franz Xaver</a>
</div> </div>
<div class="profile-organizationMember"> <div class="profile-organizationMember">
<span class="avatar unique user-popover size-40" data-id="4" style="background-position: -27.5718417033075px -17.430732445616px;">JM</span> <span class="avatar avatar--unique user-popover size-40" data-id="4" style="background-position: -27.5718417033075px -17.430732445616px;">JM</span>
<a href="#">Julius Müller</a> <a href="#">Julius Müller</a>
</div> </div>
<div class="profile-organizationMember"> <div class="profile-organizationMember">
<span class="avatar unique user-popover size-40" data-id="4" style="background-position: -97.5718417033075px -178.430732445616px;">HH</span> <span class="avatar avatar--unique user-popover size-40" data-id="4" style="background-position: -97.5718417033075px -178.430732445616px;">HH</span>
<a href="#">Hans Hubert</a> <a href="#">Hans Hubert</a>
</div> </div>
</div> </div>

View file

@ -52,7 +52,7 @@
</div> </div>
<div class="page-header"> <div class="page-header">
<div class="flex vertical center"> <div class="flex vertical center">
<span class="avatar unique user-popover size-50" data-id="2" style="background-position: -92.79607555375712px -106.24902447601627px;" data-original-title="" title="">NB</span> <span class="avatar avatar--unique user-popover size-50" data-id="2" style="background-position: -92.79607555375712px -106.24902447601627px;" data-original-title="" title="">NB</span>
<div class="ticket-title"> <div class="ticket-title">
<h1 contenteditable="true" class="ticket-title-update" data-placeholder="Enter Title...">Welcome to Zammad! We want to entertain you and your whole family!</h1> <h1 contenteditable="true" class="ticket-title-update" data-placeholder="Enter Title...">Welcome to Zammad! We want to entertain you and your whole family!</h1>
</div> </div>
@ -76,7 +76,7 @@
</div> </div>
</div> </div>
<div class="article-content"> <div class="article-content">
<span class="avatar unique user-popover " data-id="2" style="background-position: -96.5079185759074px -112.28590086669901px;" data-placement="left" data-original-title="" title="">NB</span> <span class="avatar avatar--unique user-popover " data-id="2" style="background-position: -96.5079185759074px -112.28590086669901px;" data-placement="left" data-original-title="" title="">NB</span>
<div class="flex bubble-gap internal-border"> <div class="flex bubble-gap internal-border">
<div class="textBubble"> <div class="textBubble">
<div class="bubble-arrow"></div> <div class="bubble-arrow"></div>
@ -182,7 +182,7 @@
</div> </div>
</div> </div>
<div class="article-content"> <div class="article-content">
<span class="avatar unique user-popover " data-id="2" style="background-position: -96.5079185759074px -112.28590086669901px;" data-placement="left" data-original-title="" title="">NB</span> <span class="avatar avatar--unique user-popover " data-id="2" style="background-position: -96.5079185759074px -112.28590086669901px;" data-placement="left" data-original-title="" title="">NB</span>
<div class="flex bubble-gap internal-border"> <div class="flex bubble-gap internal-border">
<div class="textBubble"><div class="bubble-arrow"></div><div class="article-text" id="article-35098758344">Ich wollte mir die Lyrics von Maria herunterladen, aber ich schaff es einfach nicht, da raufzukommen. Schick mir bitte mein Passwort. <div class="textBubble"><div class="bubble-arrow"></div><div class="article-text" id="article-35098758344">Ich wollte mir die Lyrics von Maria herunterladen, aber ich schaff es einfach nicht, da raufzukommen. Schick mir bitte mein Passwort.
@ -547,7 +547,7 @@
<hr> <hr>
<div class="sidebar-content"> <div class="sidebar-content">
<div class="sidebar-block"> <div class="sidebar-block">
<span class="avatar unique userInfo-avatar size-50 user-popover" data-id="2" style="background-position: -92.79607555375712px -106.24902447601627px;">NB</span> <span class="avatar avatar--unique userInfo-avatar size-50 user-popover" data-id="2" style="background-position: -92.79607555375712px -106.24902447601627px;">NB</span>
<h3 title="Name"> <h3 title="Name">
Nicole Braun Nicole Braun
</h3> </h3>
@ -604,7 +604,7 @@
<label>Members</label> <label>Members</label>
<div class="userList"> <div class="userList">
<div class="userList-entry"> <div class="userList-entry">
<span class="avatar unique size-40 user-popover" data-id="2" data-original-title="" style="background-position: -96.5079185759074px -112.28590086669901px;" title="">NB</span> <a class="userList-entry user-popover" data-id="2" data-original-title="" href="#user/profile/2" title="">Nicole Braun</a> <span class="avatar avatar--unique size-40 user-popover" data-id="2" data-original-title="" style="background-position: -96.5079185759074px -112.28590086669901px;" title="">NB</span> <a class="userList-entry user-popover" data-id="2" data-original-title="" href="#user/profile/2" title="">Nicole Braun</a>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,3 +1,4 @@
<div class="flex horizontal js-avatars"></div>
<div class="buttonDropdown btn js-reset <% if !@resetButtonShown: %>hide<% end %>"><%- @T('Discard your unsaved changes.') %></div> <div class="buttonDropdown btn js-reset <% if !@resetButtonShown: %>hide<% end %>"><%- @T('Discard your unsaved changes.') %></div>
<div class="buttonDropdown dropdown dropdown--actions dropup"> <div class="buttonDropdown dropdown dropdown--actions dropup">
<div class="btn btn--text btn--icon--last" data-toggle="dropdown"> <div class="btn btn--text btn--icon--last" data-toggle="dropdown">

View file

@ -3508,6 +3508,20 @@ footer {
fill: hsl(47,100%,59%); fill: hsl(47,100%,59%);
} }
&-status {
position: absolute;
right: -4px;
bottom: -4px;
border-radius: 999px;
background: hsl(234,10%,19%);
fill: white;
width: 21px;
height: 21px;
display: flex;
align-items: center;
justify-content: center;
}
&.size-50 { &.size-50 {
width: 50px; width: 50px;
height: 50px; height: 50px;
@ -3530,7 +3544,12 @@ footer {
} }
} }
&.unique { &--idle {
filter: grayscale(100%);
opacity: 0.5;
}
&--unique {
background-image: image_url("/assets/images/avatar-bg.png"); background-image: image_url("/assets/images/avatar-bg.png");
background-size: 300px 226px; background-size: 300px 226px;
color: white; color: white;
@ -5236,6 +5255,10 @@ footer {
margin: 20px 0; margin: 20px 0;
} }
.tags {
margin-top: 10px;
}
.userNotifications label + .btn { .userNotifications label + .btn {
margin-top: 1px; margin-top: 1px;
} }
@ -8502,6 +8525,11 @@ body.fit {
margin-right: auto; margin-right: auto;
} }
.half-spacer {
width: 5px;
height: 5px;
}
.spacer { .spacer {
width: 10px; width: 10px;
height: 10px; height: 10px;

View file

@ -4,16 +4,13 @@ class TaskbarController < ApplicationController
before_action :authentication_check before_action :authentication_check
def index def index
current_user_tasks = Taskbar.where(user_id: current_user.id) current_user_tasks = Taskbar.where(user_id: current_user.id)
model_index_render_result(current_user_tasks) model_index_render_result(current_user_tasks)
end end
def show def show
taskbar = Taskbar.find(params[:id]) taskbar = Taskbar.find(params[:id])
access(taskbar) access(taskbar)
model_show_render_item(taskbar) model_show_render_item(taskbar)
end end
@ -24,7 +21,6 @@ class TaskbarController < ApplicationController
def update def update
taskbar = Taskbar.find(params[:id]) taskbar = Taskbar.find(params[:id])
access(taskbar) access(taskbar)
taskbar.update_attributes!(Taskbar.param_cleanup(params)) taskbar.update_attributes!(Taskbar.param_cleanup(params))
model_update_render_item(taskbar) model_update_render_item(taskbar)
end end
@ -32,7 +28,6 @@ class TaskbarController < ApplicationController
def destroy def destroy
taskbar = Taskbar.find(params[:id]) taskbar = Taskbar.find(params[:id])
access(taskbar) access(taskbar)
taskbar.destroy taskbar.destroy
model_destroy_render_item() model_destroy_render_item()
end end

View file

@ -3,16 +3,103 @@
class Taskbar < ApplicationModel class Taskbar < ApplicationModel
store :state store :state
store :params store :params
before_create :update_last_contact, :set_user store :preferences
before_update :update_last_contact, :set_user before_create :update_last_contact, :set_user, :update_preferences_infos
before_update :update_last_contact, :set_user, :update_preferences_infos
after_update :notify_clients
after_destroy :update_preferences_infos, :notify_clients
attr_accessor :local_update
def state_changed?
return false if !state
return false if state.empty?
state.each { |_key, value|
if value.class == Hash || value.class == ActiveSupport::HashWithIndifferentAccess
value.each { |_key1, value1|
next if value1 && value1.empty?
return true
}
else
next if value && value.empty?
return true
end
}
false
end
private private
def update_last_contact def update_last_contact
return true if local_update
self.last_contact = Time.zone.now self.last_contact = Time.zone.now
end end
def set_user def set_user
return true if local_update
self.user_id = UserInfo.current_user_id self.user_id = UserInfo.current_user_id
end end
def update_preferences_infos
return true if local_update
# find other same open tasks
preferences[:tasks] = []
Taskbar.where(key: key).order(:created_at).each { |taskbar|
changed = if taskbar.id == id
state_changed?
else
taskbar.state_changed?
end
task = {
id: taskbar.id,
user_id: taskbar.user_id,
last_contact: taskbar.last_contact,
changed: changed,
}
preferences[:tasks].push task
}
if !id
changed = state_changed?
task = {
user_id: user_id,
last_contact: last_contact,
changed: changed,
}
preferences[:tasks].push task
end
# update other taskbars
Taskbar.where(key: key).order(:created_at).each { |taskbar|
next if taskbar.id == id
taskbar.preferences = preferences
taskbar.local_update = true
taskbar.save!
}
return true if destroyed?
# remember preferences for current taskbar
self.preferences = preferences
true
end
def notify_clients
return true if !changes['preferences']
data = {
event: 'taskbar:preferences',
data: {
id: id,
key: key,
preferences: preferences,
},
}
PushMessages.send_to(
user_id,
data,
)
end
end end

View file

@ -243,6 +243,7 @@ class CreateBase < ActiveRecord::Migration
t.string :key, limit: 100, null: false t.string :key, limit: 100, null: false
t.string :callback, limit: 100, null: false t.string :callback, limit: 100, null: false
t.text :state, limit: 20.megabytes + 1, null: true t.text :state, limit: 20.megabytes + 1, null: true
t.text :preferences, limit: 5.megabytes + 1, null: true
t.string :params, limit: 2000, null: true t.string :params, limit: 2000, null: true
t.integer :prio, null: false t.integer :prio, null: false
t.boolean :notify, null: false, default: false t.boolean :notify, null: false, default: false
@ -251,6 +252,7 @@ class CreateBase < ActiveRecord::Migration
end end
add_index :taskbars, [:user_id] add_index :taskbars, [:user_id]
add_index :taskbars, [:client_id] add_index :taskbars, [:client_id]
add_index :taskbars, [:key]
create_table :tags do |t| create_table :tags do |t|
t.references :tag_item, null: false t.references :tag_item, null: false

View file

@ -0,0 +1,11 @@
class AddTaskbarMeta < ActiveRecord::Migration
def up
# return if it's a new setup
return if !Setting.find_by(name: 'system_init_done')
add_column :taskbars, :preferences, :text, limit: 5.megabytes + 1, null: true
add_index :taskbars, [:key]
Cache.clear
end
end

View file

@ -19,17 +19,31 @@ module PushMessages
) )
return true return true
end end
Thread.current[:push_messages].push data message = { type: 'broadcast', data: data }
Thread.current[:push_messages].push message
end
def self.send_to(user_id, data)
if !PushMessages.enabled?
Sessions.send_to(user_id, data)
return true
end
message = { type: 'send_to', user_id: user_id, data: data }
Thread.current[:push_messages].push message
end end
def self.finish def self.finish
return false if !enabled? return false if !enabled?
Thread.current[:push_messages].each { |data| Thread.current[:push_messages].each { |message|
if message[:type] == 'send_to'
Sessions.send_to(message[:user_id], message[:data])
else
Sessions.broadcast( Sessions.broadcast(
data[:message], message[:data][:message],
data[:type], message[:data][:type],
data[:current_user_id], message[:data][:current_user_id],
) )
end
} }
Thread.current[:push_messages] = nil Thread.current[:push_messages] = nil
true true

269
spec/models/taskbar_spec.rb Normal file
View file

@ -0,0 +1,269 @@
require 'rails_helper'
RSpec.describe Taskbar do
context 'single creation' do
Taskbar.destroy_all
UserInfo.current_user_id = 1
taskbar = Taskbar.create(
client_id: 123,
key: 'Ticket-1234',
callback: 'TicketZoom',
params: {
id: 1234,
},
state: {},
prio: 1,
notify: false,
)
it 'existing key' do
expect(taskbar.key).to eq('Ticket-1234')
end
it 'params' do
expect(taskbar.params[:id]).to eq(1234)
end
it 'state' do
expect(taskbar.state.empty?).to eq(true)
end
UserInfo.current_user_id = nil
end
context 'multible creation' do
it 'create tasks' do
Taskbar.destroy_all
UserInfo.current_user_id = 1
taskbar1 = Taskbar.create(
client_id: 123,
key: 'Ticket-1234',
callback: 'TicketZoom',
params: {
id: 1234,
},
state: {},
prio: 1,
notify: false,
)
UserInfo.current_user_id = 2
taskbar2 = Taskbar.create(
client_id: 123,
key: 'Ticket-1234',
callback: 'TicketZoom',
params: {
id: 1234,
},
state: {},
prio: 2,
notify: false,
)
taskbar1.reload
expect(taskbar1.preferences[:tasks].count).to eq(2)
expect(taskbar1.preferences[:tasks][0][:user_id]).to eq(1)
expect(taskbar1.preferences[:tasks][0][:changed]).to eq(false)
expect(taskbar1.preferences[:tasks][1][:user_id]).to eq(2)
expect(taskbar1.preferences[:tasks][1][:changed]).to eq(false)
taskbar2.reload
expect(taskbar2.preferences[:tasks].count).to eq(2)
expect(taskbar2.preferences[:tasks][0][:user_id]).to eq(1)
expect(taskbar2.preferences[:tasks][0][:changed]).to eq(false)
expect(taskbar2.preferences[:tasks][1][:user_id]).to eq(2)
expect(taskbar2.preferences[:tasks][1][:changed]).to eq(false)
taskbar3 = Taskbar.create(
client_id: 123,
key: 'Ticket-4444',
callback: 'TicketZoom',
params: {
id: 4444,
},
state: {},
prio: 2,
notify: false,
)
taskbar1.reload
expect(taskbar1.preferences[:tasks].count).to eq(2)
expect(taskbar1.preferences[:tasks][0][:user_id]).to eq(1)
expect(taskbar1.preferences[:tasks][0][:changed]).to eq(false)
expect(taskbar1.preferences[:tasks][1][:user_id]).to eq(2)
expect(taskbar1.preferences[:tasks][1][:changed]).to eq(false)
taskbar2.reload
expect(taskbar2.preferences[:tasks].count).to eq(2)
expect(taskbar2.preferences[:tasks][0][:user_id]).to eq(1)
expect(taskbar2.preferences[:tasks][0][:changed]).to eq(false)
expect(taskbar2.preferences[:tasks][1][:user_id]).to eq(2)
expect(taskbar2.preferences[:tasks][1][:changed]).to eq(false)
taskbar3.reload
expect(taskbar3.preferences[:tasks].count).to eq(1)
expect(taskbar3.preferences[:tasks][0][:user_id]).to eq(2)
expect(taskbar3.preferences[:tasks][0][:changed]).to eq(false)
UserInfo.current_user_id = 3
taskbar4 = Taskbar.create(
client_id: 123,
key: 'Ticket-1234',
callback: 'TicketZoom',
params: {
id: 1234,
},
state: {},
prio: 4,
notify: false,
)
taskbar1.reload
expect(taskbar1.preferences[:tasks].count).to eq(3)
expect(taskbar1.preferences[:tasks][0][:user_id]).to eq(1)
expect(taskbar1.preferences[:tasks][0][:changed]).to eq(false)
expect(taskbar1.preferences[:tasks][1][:user_id]).to eq(2)
expect(taskbar1.preferences[:tasks][1][:changed]).to eq(false)
expect(taskbar1.preferences[:tasks][2][:user_id]).to eq(3)
expect(taskbar1.preferences[:tasks][2][:changed]).to eq(false)
taskbar2.reload
expect(taskbar2.preferences[:tasks].count).to eq(3)
expect(taskbar2.preferences[:tasks][0][:user_id]).to eq(1)
expect(taskbar2.preferences[:tasks][0][:changed]).to eq(false)
expect(taskbar2.preferences[:tasks][1][:user_id]).to eq(2)
expect(taskbar2.preferences[:tasks][1][:changed]).to eq(false)
expect(taskbar2.preferences[:tasks][2][:user_id]).to eq(3)
expect(taskbar2.preferences[:tasks][2][:changed]).to eq(false)
taskbar3.reload
expect(taskbar3.preferences[:tasks].count).to eq(1)
expect(taskbar3.preferences[:tasks][0][:user_id]).to eq(2)
expect(taskbar3.preferences[:tasks][0][:changed]).to eq(false)
taskbar4.reload
expect(taskbar4.preferences[:tasks].count).to eq(3)
expect(taskbar4.preferences[:tasks][0][:user_id]).to eq(1)
expect(taskbar4.preferences[:tasks][0][:changed]).to eq(false)
expect(taskbar4.preferences[:tasks][1][:user_id]).to eq(2)
expect(taskbar4.preferences[:tasks][1][:changed]).to eq(false)
expect(taskbar4.preferences[:tasks][2][:user_id]).to eq(3)
expect(taskbar4.preferences[:tasks][2][:changed]).to eq(false)
UserInfo.current_user_id = 2
taskbar2.state = { article: {}, ticket: {} }
taskbar2.save!
taskbar1.reload
expect(taskbar1.preferences[:tasks].count).to eq(3)
expect(taskbar1.preferences[:tasks][0][:user_id]).to eq(1)
expect(taskbar1.preferences[:tasks][0][:changed]).to eq(false)
expect(taskbar1.preferences[:tasks][1][:user_id]).to eq(2)
expect(taskbar1.preferences[:tasks][1][:changed]).to eq(false)
expect(taskbar1.preferences[:tasks][2][:user_id]).to eq(3)
expect(taskbar1.preferences[:tasks][2][:changed]).to eq(false)
taskbar2.reload
expect(taskbar2.preferences[:tasks].count).to eq(3)
expect(taskbar2.preferences[:tasks][0][:user_id]).to eq(1)
expect(taskbar2.preferences[:tasks][0][:changed]).to eq(false)
expect(taskbar2.preferences[:tasks][1][:user_id]).to eq(2)
expect(taskbar2.preferences[:tasks][1][:changed]).to eq(false)
expect(taskbar2.preferences[:tasks][2][:user_id]).to eq(3)
expect(taskbar2.preferences[:tasks][2][:changed]).to eq(false)
taskbar3.reload
expect(taskbar3.preferences[:tasks].count).to eq(1)
expect(taskbar3.preferences[:tasks][0][:user_id]).to eq(2)
expect(taskbar3.preferences[:tasks][0][:changed]).to eq(false)
taskbar4.reload
expect(taskbar4.preferences[:tasks].count).to eq(3)
expect(taskbar4.preferences[:tasks][0][:user_id]).to eq(1)
expect(taskbar4.preferences[:tasks][0][:changed]).to eq(false)
expect(taskbar4.preferences[:tasks][1][:user_id]).to eq(2)
expect(taskbar4.preferences[:tasks][1][:changed]).to eq(false)
expect(taskbar4.preferences[:tasks][2][:user_id]).to eq(3)
expect(taskbar4.preferences[:tasks][2][:changed]).to eq(false)
UserInfo.current_user_id = 2
taskbar2.state = { article: { body: 'some body' }, ticket: {} }
taskbar2.save!
taskbar1.reload
expect(taskbar1.preferences[:tasks].count).to eq(3)
expect(taskbar1.preferences[:tasks][0][:user_id]).to eq(1)
expect(taskbar1.preferences[:tasks][0][:changed]).to eq(false)
expect(taskbar1.preferences[:tasks][1][:user_id]).to eq(2)
expect(taskbar1.preferences[:tasks][1][:changed]).to eq(true)
expect(taskbar1.preferences[:tasks][2][:user_id]).to eq(3)
expect(taskbar1.preferences[:tasks][2][:changed]).to eq(false)
taskbar2.reload
expect(taskbar2.preferences[:tasks].count).to eq(3)
expect(taskbar2.preferences[:tasks][0][:user_id]).to eq(1)
expect(taskbar2.preferences[:tasks][0][:changed]).to eq(false)
expect(taskbar2.preferences[:tasks][1][:user_id]).to eq(2)
expect(taskbar2.preferences[:tasks][1][:changed]).to eq(true)
expect(taskbar2.preferences[:tasks][2][:user_id]).to eq(3)
expect(taskbar2.preferences[:tasks][2][:changed]).to eq(false)
taskbar3.reload
expect(taskbar3.preferences[:tasks].count).to eq(1)
expect(taskbar3.preferences[:tasks][0][:user_id]).to eq(2)
expect(taskbar3.preferences[:tasks][0][:changed]).to eq(false)
taskbar4.reload
expect(taskbar4.preferences[:tasks].count).to eq(3)
expect(taskbar4.preferences[:tasks][0][:user_id]).to eq(1)
expect(taskbar4.preferences[:tasks][0][:changed]).to eq(false)
expect(taskbar4.preferences[:tasks][1][:user_id]).to eq(2)
expect(taskbar4.preferences[:tasks][1][:changed]).to eq(true)
expect(taskbar4.preferences[:tasks][2][:user_id]).to eq(3)
expect(taskbar4.preferences[:tasks][2][:changed]).to eq(false)
UserInfo.current_user_id = 1
taskbar1.state = { article: { body: '' }, ticket: { state_id: 123 } }
taskbar1.save!
taskbar1.reload
expect(taskbar1.preferences[:tasks].count).to eq(3)
expect(taskbar1.preferences[:tasks][0][:user_id]).to eq(1)
expect(taskbar1.preferences[:tasks][0][:changed]).to eq(true)
expect(taskbar1.preferences[:tasks][1][:user_id]).to eq(2)
expect(taskbar1.preferences[:tasks][1][:changed]).to eq(true)
expect(taskbar1.preferences[:tasks][2][:user_id]).to eq(3)
expect(taskbar1.preferences[:tasks][2][:changed]).to eq(false)
taskbar2.reload
expect(taskbar2.preferences[:tasks].count).to eq(3)
expect(taskbar2.preferences[:tasks][0][:user_id]).to eq(1)
expect(taskbar2.preferences[:tasks][0][:changed]).to eq(true)
expect(taskbar2.preferences[:tasks][1][:user_id]).to eq(2)
expect(taskbar2.preferences[:tasks][1][:changed]).to eq(true)
expect(taskbar2.preferences[:tasks][2][:user_id]).to eq(3)
expect(taskbar2.preferences[:tasks][2][:changed]).to eq(false)
taskbar3.reload
expect(taskbar3.preferences[:tasks].count).to eq(1)
expect(taskbar3.preferences[:tasks][0][:user_id]).to eq(2)
expect(taskbar3.preferences[:tasks][0][:changed]).to eq(false)
taskbar4.reload
expect(taskbar4.preferences[:tasks].count).to eq(3)
expect(taskbar4.preferences[:tasks][0][:user_id]).to eq(1)
expect(taskbar4.preferences[:tasks][0][:changed]).to eq(true)
expect(taskbar4.preferences[:tasks][1][:user_id]).to eq(2)
expect(taskbar4.preferences[:tasks][1][:changed]).to eq(true)
expect(taskbar4.preferences[:tasks][2][:user_id]).to eq(3)
expect(taskbar4.preferences[:tasks][2][:changed]).to eq(false)
UserInfo.current_user_id = nil
end
end
end

View file

@ -43,6 +43,17 @@ class AgentTicketUpdate2Test < TestCase
value: 'some level 3 <b>body</b> 123äöü', value: 'some level 3 <b>body</b> 123äöü',
) )
watch_for(
browser: browser1,
css: '.content.active .js-attributeBar .js-avatar .avatar--not-changed',
value: 'AT', # agent1
)
watch_for(
browser: browser2,
css: '.content.active .js-attributeBar .js-avatar .avatar--not-changed',
value: 'TA', # master
)
# change edit screen in instance 1 # change edit screen in instance 1
ticket_update( ticket_update(
browser: browser1, browser: browser1,
@ -58,6 +69,17 @@ class AgentTicketUpdate2Test < TestCase
no_quote: true, no_quote: true,
) )
watch_for(
browser: browser1,
css: '.content.active .js-attributeBar .js-avatar .avatar--not-changed',
value: 'AT', # agent1
)
watch_for(
browser: browser2,
css: '.content.active .js-attributeBar .js-avatar .avatar--changed',
value: 'TA', # master
)
# update ticket in instance 2 # update ticket in instance 2
ticket_update( ticket_update(
browser: browser2, browser: browser2,
@ -72,6 +94,16 @@ class AgentTicketUpdate2Test < TestCase
value: '(Discard your unsaved changes.|Verwerfen der)', value: '(Discard your unsaved changes.|Verwerfen der)',
no_quote: true, no_quote: true,
) )
watch_for(
browser: browser1,
css: '.content.active .js-attributeBar .js-avatar .avatar--changed',
value: 'AT', # agent1
)
watch_for(
browser: browser2,
css: '.content.active .js-attributeBar .js-avatar .avatar--changed',
value: 'TA', # master
)
click( click(
browser: browser2, browser: browser2,
@ -92,6 +124,17 @@ class AgentTicketUpdate2Test < TestCase
}, },
) )
watch_for(
browser: browser1,
css: '.content.active .js-attributeBar .js-avatar .avatar--not-changed',
value: 'AT', # agent1
)
watch_for(
browser: browser2,
css: '.content.active .js-attributeBar .js-avatar .avatar--changed',
value: 'TA', # master
)
# check content and edit screen in instance 1 # check content and edit screen in instance 1
watch_for( watch_for(
browser: browser2, browser: browser2,
@ -126,6 +169,17 @@ class AgentTicketUpdate2Test < TestCase
no_quote: true, no_quote: true,
) )
watch_for(
browser: browser1,
css: '.content.active .js-attributeBar .js-avatar .avatar--not-changed',
value: 'AT', # agent1
)
watch_for(
browser: browser2,
css: '.content.active .js-attributeBar .js-avatar .avatar--not-changed',
value: 'TA', # master
)
# check content in instance 2 # check content in instance 2
watch_for( watch_for(
browser: browser2, browser: browser2,
@ -160,6 +214,17 @@ class AgentTicketUpdate2Test < TestCase
no_quote: true, no_quote: true,
) )
watch_for(
browser: browser1,
css: '.content.active .js-attributeBar .js-avatar .avatar--not-changed',
value: 'AT', # agent1
)
watch_for(
browser: browser2,
css: '.content.active .js-attributeBar .js-avatar .avatar--not-changed',
value: 'TA', # master
)
# reload instances, verify again # reload instances, verify again
reload( reload(
browser: browser1, browser: browser1,
@ -195,6 +260,17 @@ class AgentTicketUpdate2Test < TestCase
no_quote: true, no_quote: true,
) )
watch_for(
browser: browser1,
css: '.content.active .js-attributeBar .js-avatar .avatar--not-changed',
value: 'AT', # agent1
)
watch_for(
browser: browser2,
css: '.content.active .js-attributeBar .js-avatar .avatar--not-changed',
value: 'TA', # master
)
# change form of ticket, reset, reload and verify in instance 2 # change form of ticket, reset, reload and verify in instance 2
ticket_update( ticket_update(
browser: browser2, browser: browser2,
@ -244,6 +320,17 @@ class AgentTicketUpdate2Test < TestCase
) )
sleep 2 sleep 2
watch_for(
browser: browser1,
css: '.content.active .js-attributeBar .js-avatar .avatar--changed',
value: 'AT', # agent1
)
watch_for(
browser: browser2,
css: '.content.active .js-attributeBar .js-avatar .avatar--not-changed',
value: 'TA', # master
)
reload( reload(
browser: browser2, browser: browser2,
) )
@ -260,6 +347,17 @@ class AgentTicketUpdate2Test < TestCase
no_quote: true, no_quote: true,
) )
watch_for(
browser: browser1,
css: '.content.active .js-attributeBar .js-avatar .avatar--changed',
value: 'AT', # agent1
)
watch_for(
browser: browser2,
css: '.content.active .js-attributeBar .js-avatar .avatar--not-changed',
value: 'TA', # master
)
task_type( task_type(
browser: browser2, browser: browser2,
type: 'stayOnTab', type: 'stayOnTab',
@ -278,6 +376,17 @@ class AgentTicketUpdate2Test < TestCase
no_quote: true, no_quote: true,
) )
watch_for(
browser: browser1,
css: '.content.active .js-attributeBar .js-avatar .avatar--not-changed',
value: 'AT', # agent1
)
watch_for(
browser: browser2,
css: '.content.active .js-attributeBar .js-avatar .avatar--not-changed',
value: 'TA', # master
)
# check if new article is empty # check if new article is empty
ticket_verify( ticket_verify(
browser: browser2, browser: browser2,